YaST Tutorials


Table of Contents

1. YaST Development in General
1.1. Before You Start Development
1.1.1. Development Environment
1.1.2. Terminology
1.1.3. YaST Directory Structure
2. Tutorial 1—Simple YaST Module
2.1. Best Practices
2.2. What the Module Should Really Do?
2.2.1. General Project Specifications and Terms
2.2.2. Tutorial Example
2.2.2.1. General Project Definition
2.2.2.2. UI Dialog Workflow
2.2.2.3. UI Dialogs
2.3. Starting Up
2.3.1. Creating a New YaST Module
2.3.2. Checking the Progress
2.3.2.1. Installing Project on the System
2.3.2.2. Running the Application
2.3.2.3. Watching YaST Logs
2.4. YaST Module Files
2.4.1. Project Files & YaST Layers
2.4.2. Internal Workflow Explanation
2.4.3. Familiarizing with Source Files
2.4.3.1. File sshd.desktop
2.4.3.2. File sshd.ycp
2.4.3.3. File wizards.ycp
2.4.3.4. File dialogs.ycp
2.4.3.5. File complex.ycp
2.4.3.6. File helps.ycp
2.4.3.7. File Sshd.ycp
2.5. Cleaning Up Skeleton for the SSHD Configuration
2.5.1. Modifying Source Files
2.5.1.1. Changes in the src/ directory:
2.5.1.1.1. Files Sshd2.pm and Makefile.am
2.5.1.1.2. File Sshd.ycp
2.5.1.1.3. File complex.ycp
2.5.1.1.4. File dialogs.ycp
2.5.1.1.5. File helps.ycp
2.5.1.1.6. File sshd.ycp
2.5.1.1.7. File sshd_auto.ycp
2.5.1.1.8. File sshd_proposal.ycp
2.5.1.1.9. File wizards.ycp
2.5.1.2. Changes in the testsuite/ directory
2.5.2. Checking the Progress
2.6. Creating User Interface
2.6.1. Dialog Wizard
2.6.2. Dialog Content
2.6.3. Standard Dialog Handling by a While-Loop
2.6.3.1. Simple Loop Example
2.7. Creating Access to the Configuration Data
2.7.1. SCR Agent
2.7.1.1. SCR Agents in General
2.7.1.2. Looking for a SCR Agent
2.7.1.3. Creating Your Own SCR Agent
2.7.1.4. Testing the SCR Agent
2.8. Using the Access to the Configuration Data
2.8.1. The Data Model
2.8.2. Reading and Writing the Configuration
2.8.2.1. Reading and Writing the SCR
2.8.2.2. Reading and Writing the sshd Service Status
2.8.2.3. General Read and Write Settings
2.8.2.4. Complete Sshd.ycp file
2.8.3. Checking the Syntax
2.8.4. YCP Module Compilation
2.9. Connecting the Configuration Data with the UI
2.9.1. Getting and Setting the Configuration Data
2.9.2. Configuration Handling
2.9.2.1. Standard File Header
2.9.2.2. Imported Modules and Included Files
2.9.2.3. Read and Write Dialogs
2.9.2.4. UI Data Initialization
2.9.2.5. Handling Add, Edit and Delete Buttons
2.9.2.6. Capturing the Current Configuration from UI
2.9.2.7. Standard End of File
2.9.3. Adding the Complex Connection into the Dialogs
2.9.3.1. Server Configuration Dialog
2.9.3.2. Login Settings Dialog
2.9.4. Checking the Progress
2.10. Mission Completed
2.10.1. Final Look & Feel
2.11. Conclusion
2.11.1. YaST Knowledge Summary
2.11.2. Downloads
2.12. Testing and Tuning

Abstract

These tutorials can help you to learn to develop your own YaST modules from scratch as quickly as possible. For more examples of YaST, see the /usr/share/YaST2/ directory.

Chapter 1. YaST Development in General

Lukas Ocilka

Chapter in general. 

Klara Cihlarova

Chapter structural correctness, logic checks, ideas and enhancements. 

1.1. Before You Start Development

Almost every YaST development needs a standard development environment with development tools. A developer should understand the terms used and know the YaST directory structure.

1.1.1. Development Environment

YaST module development is automated in many steps by a number of helper tools. However, if you want to take advantage of it, you must install some additional packages.

Here is the full list of packages needed in alphabetical order: autoconf automake blocxx-devel dejagnu expect gcc gettext-devel glibc-devel hwinfo-devel libgcj libstdc++-devel libxcrypt-devel libxml2-devel liby2util-devel ncurses-devel openssl-devel pcre-devel perl-XML-Writer pkgconfig readline-devel tcl yast2 yast2-core yast2-core-devel yast2-devtools yast2-perl-bindings yast2-testsuite zlib-devel

1.1.2. Terminology

  • YaST Module—In a general view, a YaST module is a stand-alone tool for some functionality, such as the configuration tool for an NFS server, user manager, or system monitoring tool.

  • Project—A project is a general concept for any YaST module.

  • YCP Module—Set of functionality inside a library written in the YCP language. It can have several global or local functions and variables. A YaST module can use several YCP modules. A YCP module itself can also use other modules.

  • Perl Module—The same as a YCP module, but written in Perl.

  • SCR Agent—They are the only way to read and write a configuration or execute system commands. Developer mustn't touch the system by any other way. SCR Agents are very small programs written in C, Bash, or Perl or written as scripts using the unified Ini-Agent or Any-Agent interface. YaST uses the unified SCR YCP module for accessing these agents.

  • Helper Tool—A script or binary that has some functionality for helping a developer achieve fast results by calling many annoying and repetitive commands on its own.

1.1.3. YaST Directory Structure

The directions in this tutorial touch several directories on the system. Here is the list of directories where you can find files or directories related to YaST:

/usr/lib/YaST2/                 -- helpers, libraries, and SCR agents
/usr/share/applications/YaST2/* -- .desktop files of YaST modules
/usr/share/YaST2/               -- base directory of YaST scripts, clients, and modules
/usr/share/doc/packages/yast2*  -- documentation for particular YaST packages
/var/lib/YaST2/                 -- variable data directory
/var/log/YaST2/                 -- logging directory with automatically rotated logs

Chapter 2. Tutorial 1—Simple YaST Module

Lukas Ocilka

Tutorial in general, example source code, and screenshots. 

Stanislav Visnovsky

Tutorial structure, correctness, and ideas. 

Klara Cihlarova

Tutorial structural correctness, logical checks, ideas, and enhancements. 

Table of Contents

2.1. Best Practices
2.2. What the Module Should Really Do?
2.2.1. General Project Specifications and Terms
2.2.2. Tutorial Example
2.2.2.1. General Project Definition
2.2.2.2. UI Dialog Workflow
2.2.2.3. UI Dialogs
2.3. Starting Up
2.3.1. Creating a New YaST Module
2.3.2. Checking the Progress
2.3.2.1. Installing Project on the System
2.3.2.2. Running the Application
2.3.2.3. Watching YaST Logs
2.4. YaST Module Files
2.4.1. Project Files & YaST Layers
2.4.2. Internal Workflow Explanation
2.4.3. Familiarizing with Source Files
2.4.3.1. File sshd.desktop
2.4.3.2. File sshd.ycp
2.4.3.3. File wizards.ycp
2.4.3.4. File dialogs.ycp
2.4.3.5. File complex.ycp
2.4.3.6. File helps.ycp
2.4.3.7. File Sshd.ycp
2.5. Cleaning Up Skeleton for the SSHD Configuration
2.5.1. Modifying Source Files
2.5.1.1. Changes in the src/ directory:
2.5.1.1.1. Files Sshd2.pm and Makefile.am
2.5.1.1.2. File Sshd.ycp
2.5.1.1.3. File complex.ycp
2.5.1.1.4. File dialogs.ycp
2.5.1.1.5. File helps.ycp
2.5.1.1.6. File sshd.ycp
2.5.1.1.7. File sshd_auto.ycp
2.5.1.1.8. File sshd_proposal.ycp
2.5.1.1.9. File wizards.ycp
2.5.1.2. Changes in the testsuite/ directory
2.5.2. Checking the Progress
2.6. Creating User Interface
2.6.1. Dialog Wizard
2.6.2. Dialog Content
2.6.3. Standard Dialog Handling by a While-Loop
2.6.3.1. Simple Loop Example
2.7. Creating Access to the Configuration Data
2.7.1. SCR Agent
2.7.1.1. SCR Agents in General
2.7.1.2. Looking for a SCR Agent
2.7.1.3. Creating Your Own SCR Agent
2.7.1.4. Testing the SCR Agent
2.8. Using the Access to the Configuration Data
2.8.1. The Data Model
2.8.2. Reading and Writing the Configuration
2.8.2.1. Reading and Writing the SCR
2.8.2.2. Reading and Writing the sshd Service Status
2.8.2.3. General Read and Write Settings
2.8.2.4. Complete Sshd.ycp file
2.8.3. Checking the Syntax
2.8.4. YCP Module Compilation
2.9. Connecting the Configuration Data with the UI
2.9.1. Getting and Setting the Configuration Data
2.9.2. Configuration Handling
2.9.2.1. Standard File Header
2.9.2.2. Imported Modules and Included Files
2.9.2.3. Read and Write Dialogs
2.9.2.4. UI Data Initialization
2.9.2.5. Handling Add, Edit and Delete Buttons
2.9.2.6. Capturing the Current Configuration from UI
2.9.2.7. Standard End of File
2.9.3. Adding the Complex Connection into the Dialogs
2.9.3.1. Server Configuration Dialog
2.9.3.2. Login Settings Dialog
2.9.4. Checking the Progress
2.10. Mission Completed
2.10.1. Final Look & Feel
2.11. Conclusion
2.11.1. YaST Knowledge Summary
2.11.2. Downloads
2.12. Testing and Tuning

Abstract

This tutorial shows how to create your own YaST Module as simply as possible. It is supposed to be a basic tutorial for a broad, but not deep, overview of developing a new YaST module in YCP. It contains links where you can find more detailed information for the particular topic.

2.1. Best Practices

Before starting the development of a YaST module, think about the answers for these questions:

  • What do you expect from this YaST module?

  • What do you want to configure and how?

  • Is YaST suitable for that?

Understanding the service configuration or application used is essential. It avoids future problems with modifications of user interface, API, or internal data structures.

2.2. What the Module Should Really Do?

Project specification is a very important step in module development. The simple example shows how the project specification could be written.

2.2.1. General Project Specifications and Terms

Project specification is a very important part of every YaST Module. It shows whether it is worth the effort to develop such a YaST Module. It could also an insolvable problem before development starts or help find the best way to go in development.

YaST, in general, is divided into several layers. Some of them can run on different computers:

  • User Interface—A Graphical or textual user interface.

  • UI Handling—Handles all user's events, dialogs and dialog workflow.

  • Business Logic—Provides the global functional API and global variables (functional interface is the prefered one). Takes care about internal data handling and structure and access to SCR agents.

  • System Agents—Small programs for accessing the system files, databases, system commands, etc. They are called SCR agents.

It is quite common for bigger projects to have functions divided into several modules that can be written in YCP or Perl.

2.2.2. Tutorial Example

This example prepares to develop a YaST module for configuration of the SSH daemon. Its configuration is stored in the /etc/ssh/sshd_config file.

2.2.2.1. General Project Definition

[Important]Important

Keep in mind that this is only an example, not a real expert configuration tool. Features have been selected to keep the project extremely simple.

Chosen areas of configuration are:

  • Server behavior with the options Port, AllowTcpForwarding, X11Forwarding, and Compression

  • Login settings with the options PrintMotd and PermitRootLogin

  • Login features with the options MaxAuthTries, PasswordAuthentication, RSAAuthentication and PubkeyAuthentication

2.2.2.2. UI Dialog Workflow

This module will use only two dialogs arranged in a wizard sequence. Both dialogs will be based on the typical YaST dialog with a Back, Abort, and Next buttons and the help text in the frame on the left side of the dialog. These dialogs will be surrounded by Read dialog with a progress bar, where the current configuration is read, and Write dialog where a new configuration is written.

This is the design of the proposed workflow:

This workflow also shows that user can abort the configuration on every screen by clicking the Abort button. After this button is pressed, a pop-up dialog appears asking whether to abort the configuration. When this is confirmed, the configuration is canceled and program is terminated without saving changes.

2.2.2.3. UI Dialogs

The first dialog will contain a table of TCP ports used by the SSH daemon and its general features. The second one will contain all login settings.

Drawing of the first dialog:

The Back button will be disabled in the first dialog and the Next button will be replaced with the Accept button in the last dialog as it appears in every YaST module written strictly following the Program Text Style Guide.

Drawing of the second dialog:

2.3. Starting Up

The next step is to begin actual development.

2.3.1. Creating a New YaST Module

The fastest way to create a new YaST module is to use the powerful y2tool script that, among many other things, can generate the directory structure for a new module and fills it with standard project files from a template. This tool is a part of yast2-devtools package.

To view all available options for y2tool, enter the command:

/usr/bin/y2tool --help

You should get something similar to this list

create-new-package   gettextdomains       pot-spellcheck  y2autoconf
create-spec          check_icons          rny2rnc         y2automake
devtools-migration   checkin-stable       showy2log       y2compile
for-proofread        check-textdomain     svnall          y2makeall
generateYCPWrappers  check_ycp            tagversion      y2makepot
get-lib              kill-leftover-stuff  version

We want to create a new YaST module (package), so the most suitable option seems to be the create-new-package option. To view all suboptions for it, enter the command

/usr/bin/y2tool create-new-package

This is what you should get

Usage:
   create-new-package [-dsv] <skeleton> <name> <maintainer> <email>

   -v   verbose
   -d   debug
   -s   list of available skeletons

   skeleton   - the one which should be used (config, trans, ...)
   name       - of the component. A package name will be constructed
                like yast2-skeleton-name
   maintainer - his name
   email      - maintainer's, of course :-)

This command will create a new standard configuration module called sshd with John The Fish as the author:

/usr/bin/y2tool create-new-package -v config sshd "John The Fish" "john@thesmallfish.net"

You will get this structure of directories:

sshd
     agents    - SCR Agents
     doc       - Auto-generated documentation
     package   - Special directory for building RPM package with a .changes file
     src       - Project source files
     testsuite - Project automatic testsuites

You can download the just created package here.

2.3.2. Checking the Progress

We have just created a new YaST sshd configuration module but how to start and run this module? The answer is pretty easy...

2.3.2.1. Installing Project on the System

This is the way how to install the current state of the project into the system:

  1. Enter the directory with source files: cd sshd

  2. Check whether needed packages are installed and call YaST scripts needed for creating Makefiles: make -f Makefile.cvs

  3. Check syntax, comments of functions, create generated documentation, compile *.ycp modules into their binary form *.ybc: make

  4. Install all into particular directories, you have to become 'root' for that: sudo make install

[Important]Important

If there are any problems, it drops out a warning or an error message. Missing development package must be installed, syntax error must be fixed.

Once you have installed it for the first time, you only need to call sudo make install if you do some changes in the source code.

2.3.2.2. Running the Application

If there were no errors during the previous steps, you can simply run this command to open up your new YaST SSHD Configuration Tool:

sudo /sbin/yast2 sshd

or just

/sbin/yast2 sshd

After that, YaST opens up a Read dialog with a progress bar and after the configuration is read, it automatically switches to the Configuration dialog which looks like this one:

2.3.2.3. Watching YaST Logs

[Note]Note

It is very useful to watch the log files while developing and testing any YaST module or its part. You can do it as root with this command in some terminal:

/usr/bin/tail -F /var/log/YaST2/y2log

2.4. YaST Module Files

We have shown how really easy is to create a new YaST module, how to install it and run. Now you should learn more about source files inside the project: what they are good for, what they contain and what they handle.

2.4.1. Project Files & YaST Layers

This picture shows the disposal of project files to the particular layers of YaST. This is how the new YaST modules is supposed to be divided into files and corresponds with final version. For instance, the file sshd.scr doesn't exist in a newly-generated template.

2.4.2. Internal Workflow Explanation

This part is rather complex and not so important - you can skip it if you want.

What is done between calling the /sbin/yast2 sshd command and opening the UI up?

  1. Script /sbin/yast2 defines internal variables, such as LC_CTYPE. Then it checks whether you have Qt or ncurses available and calls the /usr/lib/YaST2/bin/y2base binary.

  2. Binary /usr/lib/YaST2/bin/y2base is called with "sshd qt" or "sshd ncurses" as two program arguments. The y2base checks whether there is any sshd.ycp client in the /usr/share/YaST2/clients/ directory and executes it.

  3. The sshd.ycp imports some binary modules such as Progress, Report or CommandLine, defines the command line options and includes the wizards.ycp file.

  4. The wizards.ycp file includes the dialogs.ycp file with defined dialogs and complex.ycp file with defined complex dialogs functions and then dialog sequences are defined.

  5. After closing the UI, the control is returned back to the sshd.ycp client which finishes the operation and exits.

2.4.3. Familiarizing with Source Files

You will find source files under the sshd/src/ path. All those files are copied into the /usr/share/YaST2/ path during the make install procedure. Let's clarify what is the purpose of these files on some simple examples. Files listed here are simplified, most of the content is replaced with a "..." mark. Important parts are emphasized. Files are sorted in a logical order.

2.4.3.1. File sshd.desktop

Application definitions for KDE, AutoYaST, YaST Control Center...

[Desktop Entry]
Type=Application
...
X-SuSE-YaST-Group=Misc
...
Icon=yast-sshd
Exec=/sbin/yast2 sshd
...
Name=Sshd
GenericName=sshd
...

These settings are used by another applications than the YaST SSHD Configuration itself. They define the module behavior and identification. See the YaST Desktop Files document for more details.

2.4.3.2. File sshd.ycp

A basic application client also with a command line interface definition. Command line support will be described in another tutorial.

Clients are stored under the /usr/share/YaST2/clients/ path

/**
 * File: clients/sshd.ycp
 ...
 */

{

textdomain "sshd";

/* The main () */
y2milestone ("----------------------------------------");
y2milestone ("Sshd module started");

import "Progress";
import "Report";
import "Summary";

import "CommandLine";
include "sshd/wizards.ycp";

map cmdline_description = $[
    ...
    "guihandler" : SshdSequence,
    ...
];

/* is this proposal or not? */
boolean propose = false;
...

/* main ui function */
any ret = nil;

if (propose) ret = SshdAutoSequence();
else ret = CommandLine::Run(cmdline_description);
y2debug("ret=%1", ret);

/* Finish */
y2milestone("Sshd module finished");
y2milestone("----------------------------------------");

return ret;

/* EOF */
}

This client is called by YaST binary, it includes the wizard.ycp file with defined sequences and calls the CommandLine::Run() wich, in our case, calls the SshdSequence() workflow from wizard.ycp.

[Note]Note

We have also other clients <module-name>_auto.ycp for the AutoYaST and <module-name>_proposal.ycp for the installation proposal but they aren't needed for this tutorial.

2.4.3.3. File wizards.ycp

Contains dialogs sequence definitions that are used by clients.

Wizards are stored under the /usr/share/YaST2/include/<module-name>/ path.

/**
 * File: include/sshd/wizards.ycp
 ...
 */

{

textdomain "sshd";

import "Sequencer";
import "Wizard";

include "sshd/complex.ycp";
include "sshd/dialogs.ycp";

/**
 * Add a configuration of sshd
 * @return sequence result
 */
any AddSequence() {
    ...
}

/**
 * Main workflow of the sshd configuration
 * @return sequence result
 */
any MainSequence() {

    map aliases = $[
	"summary"	:   ``( SummaryDialog() ),
	"overview"	:   ``( OverviewDialog() ),
	"add"		: [ ``( AddSequence() ), true ],
	"edit"		: [ ``( AddSequence() ), true ]
    ];

    map sequence = $[
	"ws_start" : "summary",
	"summary" : $[
	    `abort	: `abort,
	    `next	: `next,
	    `overview	: "overview",
	],
	"overview" : $[
	    `abort	: `abort,
	    `next	: `next,
	    `add	: "add",
	    `edit	: "edit",
	],
	"add" : $[
	    `abort	: `abort,
	    `next	: "overview",
	],
	"edit" : $[
	    `abort	: `abort,
	    `next	: "overview",
	]
    ];

    any ret = Sequencer::Run(aliases, sequence);

    return ret;
}

/**
 * Whole configuration of sshd
 * @return sequence result
 */
any SshdSequence() {

    map aliases = $[
	"read"	: [ ``( ReadDialog() ), true ],
	"main"	:   ``( MainSequence() ),
	"write"	: [ ``( WriteDialog() ), true ]
    ];

    map sequence = $[
	"ws_start" : "read",
	"read" : $[
	    `abort	: `abort,
	    `next	: "main"
	],
	"main" : $[
	    `abort	: `abort,
	    `next	: "write"
	],
	"write" : $[
	    `abort	: `abort,
	    `next	: `next
	]
    ];

    Wizard::CreateDialog();

    any ret = Sequencer::Run(aliases, sequence);

    UI::CloseDialog();
    return ret;
}

/**
 * Whole configuration of sshd but without reading and writing.
 * For use with autoinstallation.
 * @return sequence result
 */
any SshdAutoSequence() {
    ...
}

/* EOF */
}

First of all, complex.ycp and dialogs.ycp are included because they define dialogs that are used in dialog sequences.

After that, aliases to these dialogs are defined in aliases map. Then sequences pointing to these aliases are defined in a sequence map. Sequence handlers defines the relationship between event returned by a dialog function and action called by sequencer.

At the end, the SshdSequence() is called from the sshd.ycp client.

[Important]Important

Never define the `back event, dialog wizard handles it by itself.

[Note]Note

Please, take note, that sequences can call another ones: SshdSequence() calls MainSequence() and that one calls AddSequence().

Sequence called from a client should contain Wizard::CreateDialog() and UI::CloseDialog() because user should see the UI as soon as possible. Additionally Wizard::CreateDialog() creates the classic Wizard window with help text on the left side, space for dialogs on the other one and Back, Abort and Next buttons.

Screenshot of the Wizard window:

2.4.3.4. File dialogs.ycp

Dialogs definitions and their simple handling.

Dialogs are stored under the /usr/share/YaST2/include/<module-name>/ path

/**
 * File: include/sshd/dialogs.ycp
 ...
 */

{

textdomain "sshd";

import "Label";
import "Wizard";
import "Sshd";

include "sshd/helps.ycp";

/**
 * Configure1 dialog
 * @return dialog result
 */
any Configure1Dialog () {

    /* Sshd configure1 dialog caption */
    string caption = _("Sshd Configuration");

    /* Sshd configure1 dialog contents */
    term contents = `Label (_("First part of configuration of sshd"));

    Wizard::SetContentsButtons(caption, contents, HELPS["c1"]:"",
	    Label::BackButton(), Label::NextButton());

    any ret = nil;
    while (true) {

	ret = UI::UserInput();

	/* abort? */
	if(ret == `abort || ret == `cancel) {
	    if(ReallyAbort()) break;
	    else continue;
	}
        else if(ret == `next || ret == `back) {
            break;
        }
        else {
            y2error("unexpected retcode: %1", ret);
            continue;
        }
    }

    return ret;
}

/**
 * Configure2 dialog
 * @return dialog result
 */
any Configure2Dialog () {
    ...
}

/* EOF */
}

This file defines two dialog functions Configure1Dialog() and Configure2Dialog(). These functions set the behavior by calling the Wizard::SetContentsButtons() function. We can use this advanced function because we have used the Wizard::CreateDialog() in the wizards.ycp.

After the dialog is created there is a loop function that handles the user input.

Help texts are included from the helps.ycp as the HELPS map.

[Note]Note

Every string we want to mark for translation uses the _("...") notation. Every such string should have a comment for translators defined above. We use standard gettext style.

Translations insist on defined textdomain inside the file.

2.4.3.5. File complex.ycp

YCP include which contains complex functions for dialogs handling.

/**
 * File: include/sshd/complex.ycp
 ...
 */

{

textdomain "sshd";

import "Label";
...
import "Sshd";


include "sshd/helps.ycp";

...

/**
 * Read settings dialog
 * @return `abort if aborted and `next otherwise
 */
symbol ReadDialog() {
    Wizard::RestoreHelp(HELPS["read"]:"");
    // Sshd::AbortFunction = PollAbort;
    if (!Confirm::MustBeRoot()) return `abort;
    boolean ret = Sshd::Read();
    return ret ? `next : `abort;
}

/**
 * Write settings dialog
 * @return `abort if aborted and `next otherwise
 */
symbol WriteDialog() {
    ...
}

/**
 * Summary dialog
 * @return dialog result
 */
any SummaryDialog() {
    ..
}

/**
 * Overview dialog
 * @return dialog result
 */
any OverviewDialog() {

    /* Sshd overview dialog caption */
    string caption = _("Sshd Overview");

    list overview = Sshd::Overview();

    /* FIXME table header */
    term contents = Wizard_hw::ConfiguredContent(
	/* Table header */
	`header(_("Number"), _("Sshd")),
	overview, nil, nil, nil, nil );

    contents = Wizard_hw::SpacingAround(contents, 1.5, 1.5, 1.0, 1.0);

    Wizard::SetContentsButtons(caption, contents, HELPS["overview"]:"",
	    Label::BackButton(), Label::FinishButton());

    any ret = nil;
    while (true) {

	ret = UI::UserInput();

	/* abort? */
	if(ret == `abort || ret == `cancel) {
	    if(ReallyAbort()) break;
	    else continue;
	}
        /* add */
        else if(ret == `add_button) {
            ret = `add;
            break;
        }
        /* edit */
        else if(ret == `edit_button) {
            ret = `edit;
            break;
        }
        /* delete */
        else if(ret == `delete_button) {
            continue;
        }
        else if(ret == `next || ret == `back) {
            break;
        }
        else {
            y2error("unexpected retcode: %1", ret);
            continue;
        }
    }

    return ret;
}

/* EOF */
}

This file is very similar to the dialogs.ycp one but here should be more complex dialogs with handling.

The most important is the ReadDialog() that calls the Sshd::Read()—a global function from the Sshd YCP module.

Dialog functions defined in the file can be seen used in the wizards.ycp file in map of aliases.

2.4.3.6. File helps.ycp

Help texts for every dialog.

Helps are stored under the /usr/share/YaST2/include/<module-name>/ path

/**
 * File: include/sshd/helps.ycp
 ...
 */

{

textdomain "sshd";

/**
 * All helps are here
 */
map HELPS = $[

    /* Read dialog help 1/2 */
    "read" : _("<p><b><big>Initializing sshd Configuration</big></b><br>
Please wait...<br></p>
") +

    /* Read dialog help 2/2 */
    _("<p><b><big>Aborting Initialization:</big></b><br>
Safely abort the configuration utility by pressing <b>Abort</b> now.</p>
"),

...

    /* Configure1 dialog help 1/2 */
    "c1" : _("<p><b><big>Configuration Part One</big></b><br>
Press <b>Next</b> to continue.
<br></p>") +

    /* Configure1 dialog help 2/2 */
    _("<p><b><big>Selecting Something</big></b><br>
It is not possible. You must code it first. :-)
</p>"),

...

];

/* EOF */
}

This file is included by the dialogs.ycp and complex.ycp. All texts are defined in the HELPS map.

All help texts are written in HTML but they must follow the Text Style Guide.

[Note]Note

Please, take note, that help texts are divided into smaller chunks by logic parts. For translators, it's also easier to translate only a short string after you change something.

2.4.3.7. File Sshd.ycp

YCP module with global API that can be used from another modules or projects.

All modules are stored under the /usr/share/YaST2/modules/ path.

/**
 * File:        modules/Sshd.ycp
 * Package:     Configuration of sshd
 * Summary:     Sshd settings, input and output functions
 * Authors:     John The Fish <john@thesmallfish.net>
 *
 * $Id: Sshd.ycp 27914 2006-02-13 14:32:08Z jtf $
 *
 * Representation of the configuration of sshd.
 * Input and output routines.
 */

{

module "Sshd";
textdomain "sshd";

...

/**
 * Read all sshd settings
 * @return true on success
 */
global boolean Read() {
    /* Sshd read dialog caption */
    string caption = _("Initializing sshd Configuration");
    ...
    integer steps = 4;
    ...
    Progress::New( caption, " ", steps, [
	...
	], [
	...
	],
	""
    );
    ...
    Progress::NextStage();
    ...
    if(Abort()) return false;
    modified = false;
    return true;
}

/**
 * Write all sshd settings
 * @return true on success
 */
global boolean Write() {
    /* Sshd write dialog caption */
    string caption = _("Saving sshd Configuration");
    ...
    integer steps = 4;
    ...
    Progress::New( caption, " ", steps, [
	...
	], [
	...
	],
	""
    );
    ...
    Progress::NextStage();
    ...
    if(Abort()) return false;
    return true;
}

...
}

The module keyword sets the module namespace. You can acces the global variables and function with the module_name:: prefix (e.g., Sshd:: for Sshd module).

Global Read and Write functions also define the Progress bar that is changed by calling Progress::NextStage() function.

Read and Write functions should always return a boolean whether they succeeded or failed.

[Note]Note

Our project also contains the Sshd2.pm file that is a module written in a Perl language. Usage of a Perl module is is the same as usage of a YCP one but the internal syntax is different. Creating Perl modules will be explained in another tutorial.

2.5. Cleaning Up Skeleton for the SSHD Configuration

Now you know what can you find in every source file in your YaST module. This part will show you how to clean them up by removing functions and files unneeded for our module and a basic behavior and UI for SSHD Configuration will be set up.

2.5.1. Modifying Source Files

This part will summarize functions and files removed from our YaST module created using the y2tool helper.

2.5.1.1. Changes in the src/ directory:

You can download the changed source files here or you can download them one by one later in this section.

2.5.1.1.1. Files Sshd2.pm and Makefile.am

The file Sshd2.pm has been completely removed from the project and also the appropriate record in the Makefile.am file should be changed to reflect on that change because installing files using the broken Makefile.am would lead into erroneous result.

Original version:

module_DATA =                   \ 
       Sshd.ycp                \ 
       Sshd2.pm

New version:

module_DATA =                   \ 
       Sshd.ycp
2.5.1.1.2. File Sshd.ycp
[Note]Note

This configuration module will provide the basic functional interface with no global variables.

The main reason for a functional interface is that you can simply control your configuration data flow. Global variables are more vulnerable to corruption by other modules.

You had better download the changed file Sshd.ycp here

Here you can see the list of changes in the original file:

Cleaning the source code from unneeded functions:

  • Remove Modified() function (both definitions global boolean Modified...).

  • Remove AbortFunction() function.

  • Remove Abort() function.

  • Remove Export() function.

  • Remove Summary() function.

  • Remove Overview() function.

  • Remove AutoPackages() function.

  • Remove Import() function.

New and modified functionality:

  • Remove the global keyword from all definitions of variables to make them local-only because we don't want anybody to change our data internally but using the functions.

    Original:

    global boolean modified = false;

    Changed:

    boolean modified = false;
  • Add this

    import "Service";
    import "Popup";

    under other imports in the module. We are using these modules later in the module.

  • Add new variable definition

    integer sl = 1000;

    after the boolean modified... definition. This variable is used for length definition of short sleeps between read and write progress steps.

  • Add new GetModified() function just after the integer sl = 1000; definition:

    /**
     * Returns whether the configuration has been modified.
     */
    global boolean GetModified() {
        return modified;
    }
  • Add new SetModified() function after the GetModified() one:

    /**
     * Sets that the configuration has been modified.
     */
    global void SetModified() {
        modified = true;
    }
  • Modify Abort() function:

    /**
     * Returns a confirmation popup dialog whether user wants to really abort.
     */
    global boolean Abort() {
        return Popup::ReallyAbort(GetModified());
    }
  • Add new PollAbort() function after the Abort() function:

    /**
     * Checks whether an Abort button has been pressed.
     * If so, calls function to confirm the abort call.
     *
     * @return boolean true if abort confirmed
     */
    global boolean PollAbort() {
        if (UI::PollInput() == `abort)
    	return Abort();
    
        return false;
    }
  • Modify Read() function:

    Two useless progress steps were removed. Some texts were changed, e.g., Sshd to SSHD.

    /**
     * Read all SSHD settings
     * @return true on success
     */
    global boolean Read() {
    
        /* SSHD read dialog caption */
        string caption = _("Initializing SSHD Configuration");
    
        integer steps = 2;
    
        Progress::New( caption, " ", steps, [
        /* Progress stage 1/2 */
        _("Read current SSHD configuration"),
        /* Progress stage 2/2 */
        _("Read current SSHD state")
    ], [
        /* Progress step 1/2 */
        _("Reading current SSHD configuration..."),
        /* Progress step 2/2 */
        _("Reading current SSHD state..."),
        /* Progress finished */
        Message::Finished()
    ],
    ""
        );
    
        sleep(sl);
    
        if(PollAbort()) return false;
        Progress::NextStage();
        /* Error message */
        if(false) Report::Error(Message::CannotReadCurrentSettings());
        sleep(sl);
    
        if(PollAbort()) return false;
        Progress::NextStep();
        /* Error message */
        if(false) Report::Error(_("Cannot read current SSHD state."));
        sleep(sl);
    
        if(PollAbort()) return false;
        Progress::NextStage ();
        sleep(sl);
    
        modified = false;
        return true;
    }
  • Modify Write() function:

    Some texts were changed, e.g., Sshd to SSHD. Message::SuSEConfigFailed() is replaced with Message::CannotAdjustService("sshd").

    /**
     * Write all SSHD settings
     * @return true on success
     */
    global boolean Write() {
    
        /* SSHD read dialog caption */
        string caption = _("Saving SSHD Configuration");
    
        integer steps = 2;
        
        Progress::New(caption, " ", steps, [
        /* Progress stage 1/2 */
        _("Write the SSHD settings"),
        /* Progress stage 2/2 */
        _("Adjust the SSHD service")
    ], [
        /* Progress step 1/2 */
        _("Writing the SSHD settings..."),
        /* Progress step 2/2 */
        _("Adjusting the SSHD service..."),
        Message::Finished()
    ],
    ""
        );
    
        sleep(sl);
    
        if(PollAbort()) return false;
        Progress::NextStage();
        /* Error message */
        if(false) Report::Error (_("Cannot write SSHD settings."));
        sleep(sl);
    
        if(PollAbort()) return false;
        Progress::NextStage ();
        /* Error message */
        if(false) Report::Error (Message::CannotAdjustService("sshd"));
        sleep(sl);
    
        Progress::NextStage ();
        sleep(sl);
    
        return true;
    }
2.5.1.1.3. File complex.ycp

You had better download the changed file complex.ycp here

Here is the list of changes in the file:

Removed unneeded functions:

  • Remove Modified() function.

  • Remove ReallyAbort() function.

  • Remove PollAbort() function.

  • Remove SummaryDialog() function.

  • Remove OverviewDialog() function.

2.5.1.1.4. File dialogs.ycp

You had better download the changed file dialogs.ycp here

Here is the list of changes in the file:

Removed unneeded functions:

  • Remove Configure1Dialog() function.

  • Remove Configure2Dialog() function.

Added new functions:

  • Add new ServerConfigurationDialog() function.

    This function creates a dialog using the Wizard module and then handles the user input in the endless loop. After the Abort or Next button is pressed, handler skips outside the loop and returns the ID of the pressed button as the function result.

    /**
     * Server Configuration Dialog
     *
     * @return any dialog result
     */
    any ServerConfigurationDialog() {
    
        /* a dialog caption */
        string caption = _("SSHD Server Configuration");
    
        term contents = `VBox (
            `Left(`Label(_("SSHD TCP Ports"))),
            `Left(
                `VBox (
                    `MinSize( 40, 5,
                        /* A table header */
                        `Table(`id("Port"), `header("Port"), [])
                    ),
                    `HBox (
                        /* a push button */
                        `PushButton(`id("add_port"), _("&Add...")),
                        /* a push button */
                        `PushButton(`id("edit_port"), _("&Edit...")),
                        /* a push button */
                        `PushButton(`id("delete_port"), _("&Delete"))
                    ),
                    `VSpacing(1),
                    `Frame (
                        /* a dialog frame caption */
                        _("Server Features"),
                        `VBox (
                            /* a check box */
                            `Left(`CheckBox(`id("AllowTcpForwarding"), _("Allow &TCP Forwarding"))),
                            /* a check box */
                            `Left(`CheckBox(`id("X11Forwarding"),      _("Allow &X11 Forwarding"))),
                            /* a check box */
                            `Left(`CheckBox(`id("Compression"),        _("Allow &Compression")))
                        )
                    ),
                    `VStretch()
                )
            )
        );
    
        Wizard::SetContentsButtons(caption, contents, HELPS["server_configuration"]:"",
                Label::BackButton(), Label::NextButton());
        Wizard::DisableBackButton();
    
        any ret = nil;
        while(true) {
    
            ret = UI::UserInput();
    
            /* abort? */
            if(ret == `abort) {
                if(Sshd::Abort()) break;
                else continue;
            /* next */
            } else if(ret == `next) {
                break;
            /* unknown */
            } else {
                y2error("unexpected retcode: %1", ret);
                continue;
            }
        }
    
        Wizard::RestoreBackButton();
    
        return ret;
    }

    This dialog defines the the table of TCP Ports connected with Add..., Edit... and Delete buttons. Add... and Edit... button labels are followed by dots because pressing these buttons would open a new small pop-up dialog on the top of the current one. Server Features options are surrounded by a `Frame widget.

     

  • Add new LoginSettingsDialog() function.

    This function creates a dialog using the Wizard module and then handles the user input in the endless loop. After the Abort, Next or Back button is pressed, handler skips outside the loop and returns the ID of the pressed button as the function result.

    /**
     * Login Settings Dialog
     *
     * @return any dialog result
     */
    any LoginSettingsDialog() {
    
        /* a dialog caption */
        string caption = _("SSHD Server Login Settings");
    
        term contents = `VBox(
            `Frame (
                _("General Login Settings"),
                `VBox (
                    /* A check box */
                    `Left(`CheckBox(`id("PrintMotd"),            _("Print &Message of the Day After Login"))),
                    /* A check box */
                    `Left(`CheckBox(`id("PermitRootLogin"),      _("Permi&t Root Login")))
                )
            ),
            `VSpacing(1),
            `Frame (
                _("Authentication Settings"),
                `VBox (
                    /* A text entry */
                    `Left(`TextEntry(`id("MaxAuthTries"),           _("Ma&ximum Authentication Tries"))),
                    /* A check box */
                    `Left(`CheckBox (`id("PasswordAuthentication"), _("Pa&sswordAuthentication"))),
                    /* A check box */
                    `Left(`CheckBox (`id("RSAAuthentication"),      _("RSA Authenti&cation"))),
                    /* A check box */
                    `Left(`CheckBox (`id("PubkeyAuthentication"),   _("Public &Key Authentication")))
                )
            ),
            `VStretch()
        );
    
        Wizard::SetContentsButtons(caption, contents, HELPS["login_settings"]:"",
                Label::BackButton(), Label::NextButton());
        Wizard::SetNextButton(`next, Label::AcceptButton());
    
        any ret = nil;
        while(true) {
    
            ret = UI::UserInput();
    
            /* abort? */
            if(ret == `abort) {
                if(Sshd::Abort()) break;
                else continue;
            /* next */
            } else if(ret == `next) {
                break;
            } else if(ret == `back) {
                break;
            /* unknown */
            } else {
                y2error("unexpected retcode: %1", ret);
                continue;
            }
        }
    
        Wizard::RestoreNextButton();
    
        return ret;
    }

    This simple dialog contains two frames of General Login Settings and Authentication Settings with check boxes.

     

2.5.1.1.5. File helps.ycp

You had better download the changed file helps.ycp here

Here is the list of changes in the file:

  • Remove helps for summary, overview, c1 and c2.

  • Add new helps for server_configuration and login_settings

    /* Server Configuration dialog help */
        "server_configuration" : _("<p><b><big>Server Configuration</big></b><br>
    Configure SSHD here.<br></p>"),
    
        /* Login Settings dialog help */
        "login_settings" : _("<p><b><big>Login Settings</big></b><br>
    Configure SSHD login settings here.<br></p>"),
[Note]Note

Structure of helps is:

map HELPS = $[
       // TRANSLATORS: Comment for translators (this is a help text for...)
       "help_for" : _("Help text marked for translation..."),
       ];
2.5.1.1.6. File sshd.ycp

You had better download the changed file sshd.ycp here

Here is the list of changes in the file:

  • Remove a command line definition.

  • Remove installation proposal support.

This client still runs the workflow via CommandLine module with an empty command line definition just to inform that the command line interface is not available for this module if anyone tries to run it.

2.5.1.1.7. File sshd_auto.ycp

This file has been completely removed from the package and also the appropriate record in the Makefile.am file.

 

2.5.1.1.8. File sshd_proposal.ycp

This file has been completely removed from the package and also the appropriate record in the Makefile.am file.

Makefile.am file should be changed this way.

Original version:

client_DATA =                   \
       sshd.ycp                \
       sshd_auto.ycp   \
       sshd_proposal.ycp

New version:

client_DATA =                   \
       sshd.ycp

 

2.5.1.1.9. File wizards.ycp

You had better download the changed file wizards.ycp here

Here is the list of changes in the file:

  • Remove AddSequence() function.

  • Remove AutoSequence() function.

2.5.1.2. Changes in the testsuite/ directory

Testsuites were designed for checking the global functionality, such as functions of modules, to keep them consistent. They can also help to find out when something else, used by your module, is suddenly changed.

In this tutorial, all testsuites testsuite/tests/Sshd.* have been completely removed. Creating and running testsuites will be later described in another tutorial.

2.5.2. Checking the Progress

We have worked hard to get this project where it is and now we would like to enjoy the current status by running the application.

# Enter the basic directory of the project
cd sshd

# We have changed some Makefiles
make -f Makefile.cvs

# Check the syntax and compile YCP modules
make

# Install the project files to the system
sudo make install

# Run the application in Qt
sudo /sbin/yast2 sshd

# ... or run the application in ncurses
sudo /sbin/yast sshd

Now we can check it out. Dialogs are still not connected with the data, there are no pre-filled values and you still can't save the changed configuration, in spite of these facts, it is still a great success ;)!

See the current status of your module:

[Important]Important

After that, anytime you want to check the current status of changed source code, just run these commands:

# Enter the directory with changed files
cd sshd/src/

# Compile and install files into the system
sudo make install

# Run the application
sudo /sbin/yast2 sshd

2.6. Creating User Interface

Our project has already defined a User Interface in the scr/dialogs.ycp file. This section should explain some important parts of defining and handling the UI using some simple examples:

2.6.1. Dialog Wizard

Wizard is a YCP module which do many annoying things on its own. It can do many things by a simple function call:

  • It automatically applies the current UI theme

  • Wizard::CreateDialog() - opens up a standardized dialog with correct width and height.

  • Wizard::SetContentsButtons(caption, dialog_content, dialog_help, back_button_label, next_button_label) - sets the dialog caption, content and buttons.

  • Wizard::DisableBackButton() - hides the Back button. It needs to be restored then.

  • Wizard::RestoreBackButton() - restores the hidden Back button

  • Wizard::SetNextButton(new_id, new_label) - changes the id and label of the Next button.

  • ...

All these functions are used in the dialogs.ycp for creating and handling the dialogs.

See the Wizard module documentation for the detailed information.

2.6.2. Dialog Content

Dialogs contents are stored in variables with the term data-type.

Fore more details about creating UI, widgets and layout see the YaST2 UI Layout and Events documentation.

[Important]Important

All widgets are case-sensitive.

There can be no missing or extra comma separating widgets.

term contents = `VBox (
    `Left(`Label(_("SSHD TCP Ports"))) // <--- missing a comma
    `Left(
	...
    )
);

term contents = `VBox (
    `Left(`Label(_("SSHD TCP Ports"))),
    `Left(
	...
    ), // <--- an extra comma
);

The `Left, `Right, `Top, `Bottom and `Frame widgets accept only one term argument. If you want to put more widgets inside, surround them with the `VBox or `HBox widget:

`Frame (
        /* a dialog frame caption */
        _("Server Features"),
        `VBox (
            /* a check box */
            `Left(`CheckBox(`id("AllowTcpForwarding"), _("Allow &TCP Forwarding"))),
            /* a check box */
            `Left(`CheckBox(`id("X11Forwarding"),      _("Allow &X11 Forwarding"))),
            /* a check box */
            `Left(`CheckBox(`id("Compression"),        _("Allow &Compression")))
	)
)

2.6.3. Standard Dialog Handling by a While-Loop

Every pressed `PushButton widget returns its id on the user event query (UI::UserInput()). See AAA_All-Widgets for more details about all possible general options for widgets.

2.6.3.1. Simple Loop Example

This script creates a Wizard dialog with two push buttons and the Back-Abort-Next navigation at the bottom.

If you press any of those two push buttons, the AnnounceKeyPress() function is called and reports the event by a pop-up window.

When Back or Next button is pressed, it reports the event by a pop-up window and then the script finishes. When pressed the Abort button or when you try to close the window by your window manager's Close [x] button a confirmation of closing the script is required.

// loop_example.ycp
{
    /* --- importing needed modules --- */
    import "Wizard";
    import "Report";
    import "Label";
    import "Popup";
    /* --- importing needed modules --- */

    /* --- definition of functions --- */
    // dialog definition
    term dialog_content = `HBox (
	`PushButton(`id("pb_1"), "Button &1"),
	`PushButton(`id("pb_2"), "Button &2")
    );

    // function definition
    void AnnounceKeyPress (any key_pressed) {
	y2milestone("Pressed %1", key_pressed);
	// sformat replaces %1 with the value stored in key_pressed variable
	Report::Message(sformat("Button with id '%1' has been pressed.",
	    key_pressed)
	);
    }
    /* --- definition of functions --- */


    /* --- setting UI up --- */
    // Opening up a standard Wizard dialog
    Wizard::CreateDialog();
    Wizard::SetContentsButtons("Caption", dialog_content, "dialog help",
	Label::BackButton(), Label::NextButton());
    /* --- setting UI up --- */


    /* --- handling user input --- */
    any ret = nil;
    // starting the endless while (true) { do... } loop
    while ("hell" != "frozen over") {
	ret = UI::UserInput();
      
	// One of those dialog buttons have been pressed
	if (ret == "pb_1" || ret == "pb_2") {
	    AnnounceKeyPress(ret);

	// Back or Next navigation button have been pressed
	} else if (ret == `back || ret == `next) {
	    AnnounceKeyPress(ret);
	    // exits from the loop
	    break;

	// Confirmation question before closing the script
	} else if (ret == `abort || ret == `cancel) {
	    // exits from the loop when user confirms
	    if (Popup::ContinueCancel("Do you really want to abort?")) break;

	// unexpected return code
	} else {
	    Report::Error(sformat("Unexpected retcode %1", ret));
	}
    }
    /* --- handling user input --- */


    // Every opened dialog must be properly closed
    UI::CloseDialog();
}

2.7. Creating Access to the Configuration Data

We have already set the UI up but we don't have any real configuration data. We will have some SCR Agent for reading and writing the /etc/ssh/sshd_config configuration file.

2.7.1. SCR Agent

This section contains the general definition of any SCR Agent, way of looking for already written SCR Agent, technique how to create your own one and also the very important part about testing the SCR Agent.

2.7.1.1. SCR Agents in General

SCR is and abstraction layer. YaST never touches the system directly even if the functionality is written in Perl and such functionality exists.

Every SCR Agent is a small program which transforms any service configuration, file attribute or whatever you can imagine into the YaST data-structure. They can be written in several languages such as C, Bash, Perl or Greenlandic.

The most important functions of SCR Agent are Read() and Write() but not every SCR Agent needs to provide these functions. Additionaly, some SCR Agents have also the Execute() or Dir() function.

2.7.1.2. Looking for a SCR Agent

The first thing, we should do, is to run through all already created SCR Agents whether there is any agent we could use instead of creating a new one. After some time spent on browsing that documentation we could run across the universal .target agent which can also read and return a textual file as one string and might be also used in case when every other tries failed. Unfortunately this is not suitable for such yast-hacker you want to be. That's why we have to create our own SCR Agent.

2.7.1.3. Creating Your Own SCR Agent

Fortunately YaST has a configurable so-called IniAgent which is quite easy to use and sufficiently fulfills our needs. You can find its documentation here: /usr/share/doc/packages/yast2-core/agent-ini/ini.html (you need yast2-core-devel package installed).

Our new SCR Agent should be able to:

  • Handle repeating options such as the Port definition

  • Handle comments starting with the # string

This is the full definition of a .sshd SCR Agent which has the Read(), Write() and Dir() functions. It should be saved into the agents/sshd.scr file:

/**
 * File:
 *   sshd.scr
 * Summary:
 *   SCR Agent for reading/writing /etc/ssh/sshd_config
 *   using the ini-agent
 * Access:
 *   read/write
 * Authors:
 *   John The Fish <john@thesmallfish.net>
 * Example:
 *   Dir(.sshd)
 *   (["Port", "X11Forwarding", "Compression", "MaxAuthTries", "PermitRootLogin"])
 *
 *   Read(.sshd.Compression)
 *   (["yes"])
 *
 *   Write(.sshd.Compression, "no")
 *   (true)
 *
 * $Id: sshd.scr 11113 2005-10-20 14:15:16Z jtf $
 *
 * Fore more information about possible keys and values
 * consult with the sshd_config man pages `man sshd_config`.
 */

.sshd

`ag_ini(
    `IniAgent( "/etc/ssh/sshd_config",
        $[
            "options"   : [ "global_values", "repeat_names", "flat" ],
            "comments"  : [ "^[ \t]*#.*$", "^[ \t]*$" ],
            "params"    : [
                // Options with one value ('yes' / 'no')
                $[ "match" : [ "^[ \t]*([^ \t]+)[ \t]+([^ \t]+)[ \t]+$", "%s %s" ]],
                // Options with more possible values
                $[ "match" : [ "^[ \t]*([^ \t]+)[ \t]+(.+)[ \t]*$", "%s %s" ]],
            ],
            "subindent" : " ",
        ]
    )
)

The file agents/Makefile.am should be modified to have the sshd.scr added into the scrconf_DATA variable. This is the new version of agents/Makefile.am:

# Makefile.am for sshd/agents

agent_SCRIPTS =

scrconf_DATA = sshd.scr

EXTRA_DIST = $(agent_SCRIPTS) $(scrconf_DATA)

Run command sudo make install in the agents directory to get it installed into the /usr/share/YaST2/scrconf/ directory. Make sure, that the file has been copied there.

Every SCR Agent should have documented its functionality in the file header. Examples for every function are also very important. See the agent configuration.

For more information consult these agent's settings with the ini-agent documentation, please. This tutorial is not supposed to provide so complex information already described somewhere else.

2.7.1.4. Testing the SCR Agent

You can directly test the SCR Agent by running the y2base binary in the terminal with this command: /usr/lib/YaST2/bin/y2base stdio scr. Don't forget to watch your /var/log/YaST2/y2log file while testing.

  1. Start the y2base binary /usr/lib/YaST2/bin/y2base stdio scr returns:

    ([])
  2. Getting already configured options with command `Dir(.sshd) returns:

    (["Port", "PasswordAuthentication", "UsePAM", "X11Forwarding", "AcceptEnv", "AcceptEnv"])
  3. Reading the UsePAM option with command `Read(.sshd.UsePAM) returns:

    (["yes"])
  4. Reading all the Port options with command `Read(.sshd.Port) returns:

    (["22","33"])

Because this SCR Agent communicates using the standard I/O, you can replace the interactive call method with with the single command call using the pipe echo '`Read(.sshd.UsePAM)' | /usr/lib/YaST2/bin/y2base stdio scr which would return:

([])
(["yes"])

2.8. Using the Access to the Configuration Data

We have created our own .sshd SCR Agent in the previous section. Let's see how we can use it in our project.

In this section, we will modify the scr/Sshd.ycp YCP module.

2.8.1. The Data Model

As we could have seen during testing the .sshd SCR Agent, it returns a list of strings as values and variables are strings. So, let's decide to create an internal variable defined as map <string, list<string>>. Then we should know whether the sshd daemon was running during starting the configuration, because it this case we would have to restart it after changing the configuration file. Add this part just below the SetModified() function definition in the scr/Sshd.ycp:

/**
     * map of SSHD settings
     */
     map <string, list<string> > SETTINGS = $[];

     map <string, list<string> > DEFAULT_CONFIG = $[
     "Port"                      : ["22"],
     "AllowTcpForwarding"        : ["yes"],
     "X11Forwarding"             : ["no"],
     "Compression"               : ["yes"],
     "PrintMotd"                 : ["yes"],
     "PermitRootLogin"           : ["yes"],
     "IgnoreUserKnownHosts"      : ["no"],
     "MaxAuthTries"              : ["6"],
     "PasswordAuthentication"    : ["yes"],
     "RSAAuthentication"         : ["no"],
     "PubkeyAuthentication"      : ["yes"],
     ];

This part also defines the DEFAULT_CONFIG with default settings which we will use instead of the current one if the current one is not explicitly defined in the configuration file.

2.8.2. Reading and Writing the Configuration

This part describes using the SCR Agent inside the YCP Module, reading and writing the current sshd service status and calling this functionality from the major Read() and Write() functions.

2.8.2.1. Reading and Writing the SCR

Once we have the SCR Agent and the Data Model, we are about to create functions which will connect these two parts.

Into the upper part of the src/Sshd.ycp add the import of the SCR module:

import "SCR";

Now we can use the SCR functions.

Add this part below the PollAbort() function definition:

/**
 * Reads current sshd configuration
 */
boolean ReadSSHDSettings () {
    foreach (string key, (list <string>) SCR::Dir(.sshd), {
	list <string> val = (list <string>) SCR::Read(add(.sshd, key));
        if (val != nil) SETTINGS[key] = val;
    });
      
    y2milestone("SSHD configuration has been read: %1", SETTINGS);
    return true;
}

/**
 * Writes current sshd configuration
 */
boolean WriteSSHDSettings () {
    y2milestone("Writing SSHD configuration: %1", SETTINGS);

    foreach (string option_key, list <string> option_val, SETTINGS, {
	SCR::Write(add(.sshd, option_key), option_val);
    });
    // This is very important
    // it flushes the cache, and stores the configuration on the disk
    SCR::Write(.sshd, nil);

    return true;
}

The ReadSSHDSettings() function lists all already set options from the configuration file using the SCR::Dir() function, reads these options using the SCR::Read() function and if they have some value, stores them to the SETTINGS map.

The WriteSSHDSettings() function writes all variables from the SETTINGS map to the agent's writing cache using the SCR::Write() function and then sends the nil to the same function to store this configuration to the configuration file.

2.8.2.2. Reading and Writing the sshd Service Status

Below the previous functions we will also add functions for reading the current status of the sshd service and for restarting if it is needed:

/**
 * Reads current sshd status
 */
boolean ReadSSHDService () {
    if (Service::Status("sshd") == 0) {
	sshd_is_running = true;
    } else {
	sshd_is_running = false;
    }

    y2milestone((sshd_is_running ? "SSH is running":"SSH is not running"));
  
    return true;
}

/**
 * Restarts the sshd when the daemon was running when starting the configuration
 */
boolean WriteSSHDService () {
    boolean all_ok = true;

    if (sshd_is_running) {
	y2milestone("Restarting sshd daemon");
	all_ok = Service::Restart("sshd");
    } else {
	y2milestone("Sshd is not running - leaving...");
    }
      
    return all_ok;
}

Here is the documentation for more information about the Service module.

2.8.2.3. General Read and Write Settings

Then we have to change some lines in the general Read() and Write() functions to get the configuration read by starting the configuration and to get it written by finishing it.

Here are the changes:

  • In the Read() function:

    if(false) Report::Error(Message::CannotReadCurrentSettings());

    replace with:

    if(!ReadSSHDSettings()) Report::Error(Message::CannotReadCurrentSettings());

    and

    if(false) Report::Error(_("Cannot read current SSHD state."));

    replace with:

    if(!ReadSSHDService()) Report::Error(_("Cannot read current SSHD state."));
  • In the Write() function:

    if(false) Report::Error (_("Cannot write SSHD settings."));

    replace with:

    if(!WriteSSHDSettings()) Report::Error (_("Cannot write SSHD settings."));

    and

    if(false) Report::Error (Message::CannotAdjustService("sshd"));

    replace with:

    if(!WriteSSHDService()) Report::Error (Message::CannotAdjustService("sshd"));

2.8.2.4. Complete Sshd.ycp file

/**
 * File:	modules/Sshd.ycp
 * Package:	Configuration of SSHD
 * Summary:	SSHD settings, input and output functions
 * Authors:	John The Fish <john@thesmallfish.net>
 *
 * Representation of the configuration of SSHD.
 * Input and output routines.
 */

{

module "Sshd";
textdomain "sshd";

import "Progress";
import "Report";
import "Message";
import "Service";
import "Popup";
import "SCR";

/**
 * Data was modified?
 */
boolean modified = false;

/**
 * Sleep time between Read or Write steps
 */
integer sl = 1000;

/**
 * Returns whether the configuration has been modified.
 */
global boolean GetModified() {
    return modified;
}

/**
 * Sets that the configuration has been modified.
 */
global void SetModified() {
    modified = true;
}

/**
 * map of SSHD settings
 */
map <string, list<string> > SETTINGS = $[];

map <string, list<string> > DEFAULT_CONFIG = $[
    "Port"                      : ["22"],
    "AllowTcpForwarding"        : ["yes"],
    "X11Forwarding"             : ["no"],
    "Compression"               : ["yes"],
    "PrintMotd"                 : ["yes"],
    "PermitRootLogin"           : ["yes"],
    "IgnoreUserKnownHosts"      : ["no"],
    "MaxAuthTries"              : ["6"],
    "PasswordAuthentication"    : ["yes"],
    "RSAAuthentication"         : ["no"],
    "PubkeyAuthentication"      : ["yes"],
];

/**
 * Describes whether the daemon is running
 */
boolean sshd_is_running = false;

/**
 * Returns a confirmation popup dialog whether user wants to really abort.
 */
global boolean Abort() {
    return Popup::ReallyAbort(GetModified());
}

/**
 * Checks whether an Abort button has been pressed.
 * If so, calls function to confirm the abort call.
 *
 * @return boolean true if abort confirmed
 */
global boolean PollAbort() {
    if (UI::PollInput() == `abort)
	return Abort();

    return false;
}

/**
 * Reads current sshd configuration
 */
boolean ReadSSHDSettings () {
    foreach (string key, (list <string>) SCR::Dir(.sshd), {
        list <string> val = (list <string>) SCR::Read(add(.sshd, key));
        if (val != nil) SETTINGS[key] = val;
    });
    
    y2milestone("SSHD configuration has been read: %1", SETTINGS);
    return true;
}

/**
 * Writes current sshd configuration
 */
boolean WriteSSHDSettings () {
    y2milestone("Writing SSHD configuration: %1", SETTINGS);

    foreach (string option_key, list <string> option_val, SETTINGS, {
        SCR::Write(add(.sshd, option_key), option_val);
    });
    SCR::Write(.sshd, nil);

    return true;
}

/**
 * Reads current sshd status
 */
boolean ReadSSHDService () {
    if (Service::Status("sshd") == 0) {
        sshd_is_running = true;
    } else {
        sshd_is_running = false;
    }
    y2milestone((sshd_is_running ? "SSH is running":"SSH is not running"));
    
    return true;
}

/**
 * Restarts the sshd when the daemon was running when starting the configuration
 */
boolean WriteSSHDService () {
    boolean all_ok = true;

    if (sshd_is_running) {
        y2milestone("Restarting sshd daemon");
        all_ok = Service::Restart("sshd");
    } else {
        y2milestone("Sshd is not running - leaving...");
    }
    
    return all_ok;
}

/**
 * Read all SSHD settings
 * @return true on success
 */
global boolean Read() {

    /* SSHD read dialog caption */
    string caption = _("Initializing SSHD Configuration");

    integer steps = 2;

    Progress::New( caption, " ", steps, [
	    /* Progress stage 1/2 */
	    _("Read current SSHD configuration"),
	    /* Progress stage 2/2 */
	    _("Read current SSHD state")
	], [
	    /* Progress step 1/2 */
	    _("Reading current SSHD configuration..."),
	    /* Progress step 2/2 */
	    _("Reading current SSHD state..."),
	    /* Progress finished */
	    Message::Finished()
	],
	""
    );

    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStage();
    /* Error message */
    if(!ReadSSHDSettings()) Report::Error(Message::CannotReadCurrentSettings());
    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStep();
    /* Error message */
    if(!ReadSSHDService()) Report::Error(_("Cannot read current SSHD state."));
    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStage ();
    sleep(sl);

    modified = false;
    return true;
}

/**
 * Write all SSHD settings
 * @return true on success
 */
global boolean Write() {

    /* SSHD read dialog caption */
    string caption = _("Saving SSHD Configuration");

    integer steps = 2;
    
    Progress::New(caption, " ", steps, [
	    /* Progress stage 1/2 */
	    _("Write the SSHD settings"),
	    /* Progress stage 2/2 */
	    _("Adjust the SSHD service")
	], [
	    /* Progress step 1/2 */
	    _("Writing the SSHD settings..."),
	    /* Progress step 2/2 */
	    _("Adjusting the SSHD service..."),
	    Message::Finished()
	],
	""
    );

    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStage();
    /* Error message */
    if(!WriteSSHDSettings()) Report::Error (_("Cannot write SSHD settings."));
    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStage ();
    /* Error message */
    if(!WriteSSHDService()) Report::Error (Message::CannotAdjustService("sshd"));
    sleep(sl);

    Progress::NextStage ();
    sleep(sl);

    return true;
}

}

2.8.3. Checking the Syntax

By now, we have finished implementing the Read() and Write() functions and we should check the syntax of the the source code. This can be done by running command: /usr/bin/ycpc -E Sshd.ycp. Eventual syntax errors must be fixed.

2.8.4. YCP Module Compilation

After you finish editing the src/Sshd.ycp, run sudo make install to get it compiled and installed into the system.

Now you can check the progress as mentioned in the section Checking the Progress again. You should find the most important changes in the YaST log as mentioned in the section Watching YaST Logs.

2.9. Connecting the Configuration Data with the UI

This part will show you how to connect the Sshd module data with the UI.

2.9.1. Getting and Setting the Configuration Data

Although our Sshd module can read and write the configuration and the UI is already able to make it read or written we don't have any functionality to get the configuration from the module or to set the new options or values. Let's add the last functions to the src/Sshd.ycp. Add these functions just above the Abort() function definition:

/**
 * Returns the SSHD Option as a list of strings.
 *
 * @param string option_key of the SSHD configuration
 * @return list <string> with option_values
 */
global list <string> GetSSHDOption (string option_key) {
    return SETTINGS[option_key]:DEFAULT_CONFIG[option_key]:[];
}

/**
 * Sets values for an option.
 *
 * @param string option_key with the SSHD configuration key
 * @param list <string> option_values with the SSHD configuration values
 */
global void SetSSHDOption (string option_key, list <string> option_vals) {
    SETTINGS[option_key] = option_vals;
}

Function GetSSHDOption() reads the SETTINGS map with the parameter option_key as the key of the map. If it is not defined, it tries to find the key in the DEFAULT_CONFIG map, otherwise it selects the [] which means the empty list. The first value found is returned by the function.

Function SetSSHDOption() sets the list of strings option_vals as the value of the SETTINGS map identified by the option_key key.

Do not forget to run the sudo make install command in the src/ to compile and install the updated Sshd module

This is the final content of Sshd.ycp:

/**
 * File:	modules/Sshd.ycp
 * Package:	Configuration of SSHD
 * Summary:	SSHD settings, input and output functions
 * Authors:	John The Fish <john@thesmallfish.net>
 *
 * Representation of the configuration of SSHD.
 * Input and output routines.
 */

{

module "Sshd";
textdomain "sshd";

import "Progress";
import "Report";
import "Message";
import "Service";
import "Popup";
import "SCR";

/**
 * Data was modified?
 */
boolean modified = false;

/**
 * Sleep time between Read or Write steps
 */
integer sl = 1000;

/**
 * Returns whether the configuration has been modified.
 */
global boolean GetModified() {
    return modified;
}

/**
 * Sets that the configuration has been modified.
 */
global void SetModified() {
    modified = true;
}

/**
 * map of SSHD settings
 */
map <string, list<string> > SETTINGS = $[];

map <string, list<string> > DEFAULT_CONFIG = $[
    "Port"                      : ["22"],
    "AllowTcpForwarding"        : ["yes"],
    "X11Forwarding"             : ["no"],
    "Compression"               : ["yes"],
    "PrintMotd"                 : ["yes"],
    "PermitRootLogin"           : ["yes"],
    "IgnoreUserKnownHosts"      : ["no"],
    "MaxAuthTries"              : ["6"],
    "PasswordAuthentication"    : ["yes"],
    "RSAAuthentication"         : ["no"],
    "PubkeyAuthentication"      : ["yes"],
];

/**
 * Describes whether the daemon is running
 */
boolean sshd_is_running = false;

/**
 * Returns the SSHD Option as a list of strings.
 *
 * @param string option_key of the SSHD configuration
 * @return list <string> with option_values
 */
global list <string> GetSSHDOption (string option_key) {
    return SETTINGS[option_key]:DEFAULT_CONFIG[option_key]:[];
}

/**
 * Sets values for an option.
 *
 * @param string option_key with the SSHD configuration key
 * @param list <string> option_values with the SSHD configuration values
 */
global void SetSSHDOption (string option_key, list <string> option_vals) {
    SETTINGS[option_key] = option_vals;
}

/**
 * Returns a confirmation popup dialog whether user wants to really abort.
 */
global boolean Abort() {
    return Popup::ReallyAbort(GetModified());
}

/**
 * Checks whether an Abort button has been pressed.
 * If so, calls function to confirm the abort call.
 *
 * @return boolean true if abort confirmed
 */
global boolean PollAbort() {
    if (UI::PollInput() == `abort)
	return Abort();

    return false;
}

/**
 * Reads current sshd configuration
 */
boolean ReadSSHDSettings () {
    foreach (string key, (list <string>) SCR::Dir(.sshd), {
        list <string> val = (list <string>) SCR::Read(add(.sshd, key));
        if (val != nil) SETTINGS[key] = val;
    });
    
    y2milestone("SSHD configuration has been read: %1", SETTINGS);
    return true;
}

/**
 * Writes current sshd configuration
 */
boolean WriteSSHDSettings () {
    y2milestone("Writing SSHD configuration: %1", SETTINGS);

    foreach (string option_key, list <string> option_val, SETTINGS, {
        SCR::Write(add(.sshd, option_key), option_val);
    });
    SCR::Write(.sshd, nil);

    return true;
}

/**
 * Reads current sshd status
 */
boolean ReadSSHDService () {
    if (Service::Status("sshd") == 0) {
        sshd_is_running = true;
    } else {
        sshd_is_running = false;
    }
    y2milestone((sshd_is_running ? "SSH is running":"SSH is not running"));
    
    return true;
}

/**
 * Restarts the sshd when the daemon was running when starting the configuration
 */
boolean WriteSSHDService () {
    boolean all_ok = true;

    if (sshd_is_running) {
        y2milestone("Restarting sshd daemon");
        all_ok = Service::Restart("sshd");
    } else {
        y2milestone("Sshd is not running - leaving...");
    }
    
    return all_ok;
}

/**
 * Read all SSHD settings
 * @return true on success
 */
global boolean Read() {

    /* SSHD read dialog caption */
    string caption = _("Initializing SSHD Configuration");

    integer steps = 2;

    Progress::New( caption, " ", steps, [
	    /* Progress stage 1/2 */
	    _("Read current SSHD configuration"),
	    /* Progress stage 2/2 */
	    _("Read current SSHD state")
	], [
	    /* Progress step 1/2 */
	    _("Reading current SSHD configuration..."),
	    /* Progress step 2/2 */
	    _("Reading current SSHD state..."),
	    /* Progress finished */
	    Message::Finished()
	],
	""
    );

    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStage();
    /* Error message */
    if(!ReadSSHDSettings()) Report::Error(Message::CannotReadCurrentSettings());
    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStep();
    /* Error message */
    if(!ReadSSHDService()) Report::Error(_("Cannot read current SSHD state."));
    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStage ();
    sleep(sl);

    modified = false;
    return true;
}

/**
 * Write all SSHD settings
 * @return true on success
 */
global boolean Write() {

    /* SSHD read dialog caption */
    string caption = _("Saving SSHD Configuration");

    integer steps = 2;
    
    Progress::New(caption, " ", steps, [
	    /* Progress stage 1/2 */
	    _("Write the SSHD settings"),
	    /* Progress stage 2/2 */
	    _("Adjust the SSHD service")
	], [
	    /* Progress step 1/2 */
	    _("Writing the SSHD settings..."),
	    /* Progress step 2/2 */
	    _("Adjusting the SSHD service..."),
	    Message::Finished()
	],
	""
    );

    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStage();
    /* Error message */
    if(!WriteSSHDSettings()) Report::Error (_("Cannot write SSHD settings."));
    sleep(sl);

    if(PollAbort()) return false;
    Progress::NextStage ();
    /* Error message */
    if(!WriteSSHDService()) Report::Error (Message::CannotAdjustService("sshd"));
    sleep(sl);

    Progress::NextStage ();
    sleep(sl);

    return true;
}

}

2.9.2. Configuration Handling

Almost all of this functionality is done in the scr/complex.ycp. Here you can see the final content of the file, it will be explained below:

/**
 * File:	include/sshd/complex.ycp
 * Package:	Configuration of sshd
 * Summary:	Dialogs handling and definitions
 * Authors:	John The Fish <john@thesmallfish.net>
 *
 * $Id: complex.ycp 13891 2004-02-05 15:16:57Z jtf $
 */

{

textdomain "sshd";

import "Label";
import "Popup";
import "Wizard";
import "Wizard_hw";
import "Sshd";
import "Confirm";
import "Report";

include "sshd/helps.ycp";

/**
 * Read settings dialog
 * @return `abort if aborted and `next otherwise
 */
symbol ReadDialog() {
    Wizard::RestoreHelp(HELPS["read"]:"");
    boolean ret = Sshd::Read();
    return ret ? `next : `abort;
}

/**
 * Write settings dialog
 * @return `abort if aborted and `next otherwise
 */
symbol WriteDialog() {
    Wizard::RestoreHelp(HELPS["write"]:"");
    boolean ret = Sshd::Write();
    return ret ? `next : `abort;
}

/**
 * Initializes the table of ports
 */
void InitPortsTable () {
    list <string> ports = Sshd::GetSSHDOption("Port");
    if (ports != nil && ports != []) {
	list <term> items = [];
	foreach (string port, ports, {
	    items = add (items, `item(`id(port), port));
	});
	// Redraw table of ports and enable modification buttons
	UI::ChangeWidget(`id("Port"), `Items, items);
	UI::ChangeWidget(`id("edit_port"), `Enabled, true);
	UI::ChangeWidget(`id("delete_port"), `Enabled, true);
    } else {
	// Redraw table of ports and disable modification buttons
	UI::ChangeWidget(`id("Port"), `Items, []);
	UI::ChangeWidget(`id("edit_port"), `Enabled, false);
	UI::ChangeWidget(`id("delete_port"), `Enabled, false);
    }
}

/**
 * Initializes the Server Configuration Dialog
 */
void InitServerConfigurationDialog() {
    InitPortsTable();

    foreach (string key, ["AllowTcpForwarding", "X11Forwarding", "Compression"], {
	UI::ChangeWidget(`id(key), `Value, (Sshd::GetSSHDOption(key) == ["yes"]));
    });
}

/**
 * Initializes the Login Settings Dialog
 */
void InitLoginSettingsDialog() {
    UI::ChangeWidget( `id("MaxAuthTries"), `ValidChars, "0123456789");
    list <string> MaxAuthTries = Sshd::GetSSHDOption("MaxAuthTries");
    UI::ChangeWidget(`id("MaxAuthTries"), `Value, MaxAuthTries[0]:"0");

    foreach (string key, ["PrintMotd", "PermitRootLogin",
	"PasswordAuthentication", "RSAAuthentication", "PubkeyAuthentication"], {
	UI::ChangeWidget(`id(key), `Value, (Sshd::GetSSHDOption(key) == ["yes"]));
    });
}

/**
 * Removes the port from list of current ports
 *
 * @param string port_number
 */
void DeletePort (string port) {
    Sshd::SetSSHDOption("Port", filter (
	string single_port, Sshd::GetSSHDOption("Port"), ``(single_port != port)
    ));
}

/**
 * Function handles the adding or editing port number.
 * When the current_port is not 'nil', the dialog will
 * allow to edit it.
 *
 * @param string current_port a port number to be edited or 'nil' when adding a new one
 */
void AddEditPortDialog (string current_port) {
    UI::OpenDialog(`opt(`decorated), `VBox(
        `MinWidth (30,
	    `HBox(
		`HSpacing(1),
        	`Frame(
            	    (current_port == nil ?
			/* A popup dialog caption */
			_("Add New Port")
			:
			/* A popup dialog caption */
			_("Edit Current Port")),
		    /* A text entry */
            	    `TextEntry(`id("port_number"), _("&Port"), (current_port == nil ? "":current_port))
        	),
		`HSpacing(1)
	    )
	),
	`VSpacing(1),
	`HBox(
	    `PushButton(`id(`ok), Label::OKButton()),
	    `HSpacing(1),
	    `PushButton(`id(`cancel), Label::CancelButton())
        )
    ));

    UI::ChangeWidget( `id("port_number"), `ValidChars, "0123456789");

    any ret = nil;
    while (true) {
        ret = UI::UserInput();
        if (ret == `ok) {
            string new_port = (string) UI::QueryWidget(`id("port_number"), `Value);
	    
	    if (new_port == "") {
		UI::SetFocus(`id("port_number"));
		Report::Error(_("Port number must not be empty."));
		continue;
	    }

    	    Sshd::SetSSHDOption("Port", add (Sshd::GetSSHDOption("Port"), new_port));
	    if (current_port != nil) DeletePort(current_port);
        }
	break;
    }

    UI::CloseDialog();
}

/**
 * Function handles Add, Edit and Delete buttons
 *
 * @param any action from "add_port", "edit_port" or "delete_port"
 */
void HandleServerConfigurationDialog(any action) {
    string selected_port = (string) UI::QueryWidget(`id("Port"), `CurrentItem);

    // Adding a new port
    if (action == "add_port") {
	AddEditPortDialog(nil);
    // Editing current port
    } else if (action == "edit_port") {
	AddEditPortDialog(selected_port);
    // Deleting current port
    } else if (action == "delete_port") {
	if (Confirm::DeleteSelected()) DeletePort(selected_port);
    } else {
	y2error("Unknown action %1", action);
    }
    
    InitPortsTable();
}

/**
 * Stores the current configuration from Server Configuration Dialog
 */
void StoreServerConfigurationDialog() {
    Sshd::SetModified();

    // Stores all boolean values and turns them to the "yes"/"no" notation
    foreach (string key, ["AllowTcpForwarding", "X11Forwarding", "Compression"], {
	Sshd::SetSSHDOption(
	    key,
	    [ (((boolean) UI::QueryWidget(`id(key), `Value) == true) ? "yes":"no") ]
	);
    });
}

/**
 * Stores the current configuration from Login Settings  Dialog
 */
void StoreLoginSettingsDialog() {
    Sshd::SetModified();

    // Stores an integer value as a string
    Sshd::SetSSHDOption(
	"MaxAuthTries",
	[ (string) UI::QueryWidget(`id("MaxAuthTries"), `Value) ]
    );

    // Stores all boolean values and turns them to the "yes"/"no" notation
    foreach (string key, ["PrintMotd", "PermitRootLogin",
	"PasswordAuthentication", "RSAAuthentication", "PubkeyAuthentication"], {
	Sshd::SetSSHDOption(
	    key,
	    [ (((boolean) UI::QueryWidget(`id(key), `Value) == true) ? "yes":"no") ]
	);
    });
}

}

Functionality contained in this file is explained in parts.

2.9.2.1. Standard File Header

/**
 * File:	include/sshd/complex.ycp
 * Package:	Configuration of sshd
 * Summary:	Dialogs handling and definitions
 * Authors:	John The Fish <john@thesmallfish.net>
 *
 * $Id: complex.ycp 13891 2004-02-05 15:16:57Z jtf $
 */

{

textdomain "sshd";

This is here only for the completeness of the file. Nevertheles you can see at least the default style of the file header.

2.9.2.2. Imported Modules and Included Files

import "Label";
import "Popup";
import "Wizard";
import "Wizard_hw";
import "Sshd";
import "Confirm";
import "Report";

include "sshd/helps.ycp";

All YCP or Perl modules have their documentation here. Take note of Sshd module being imported too.

2.9.2.3. Read and Write Dialogs

/**
 * Read settings dialog
 * @return `abort if aborted and `next otherwise
 */
symbol ReadDialog() {
    Wizard::RestoreHelp(HELPS["read"]:"");
    boolean ret = Sshd::Read();
    return ret ? `next : `abort;
}

/**
 * Write settings dialog
 * @return `abort if aborted and `next otherwise
 */
symbol WriteDialog() {
    Wizard::RestoreHelp(HELPS["write"]:"");
    boolean ret = Sshd::Write();
    return ret ? `next : `abort;
}

Standard ReadDialog() and WriteDialog() functions which are called from the src/wizards.ycp file. They call the appropriate functions of the Sshd module and return a symbol to the Sequencer.

2.9.2.4. UI Data Initialization

/**
 * Initializes the table of ports
 */
void InitPortsTable () {
    list <string> ports = Sshd::GetSSHDOption("Port");

    if (ports != nil && ports != []) {
	list <term> items = [];
	foreach (string port, ports, {
	    items = add (items, `item(`id(port), port));
	});

	// Redraw table of ports and enable modification buttons
	UI::ChangeWidget(`id("Port"), `Items, items);
	UI::ChangeWidget(`id("edit_port"), `Enabled, true);
	UI::ChangeWidget(`id("delete_port"), `Enabled, true);
    } else {
	// Redraw table of ports and disable modification buttons
	UI::ChangeWidget(`id("Port"), `Items, []);
	UI::ChangeWidget(`id("edit_port"), `Enabled, false);
	UI::ChangeWidget(`id("delete_port"), `Enabled, false);
    }
}

/**
 * Initializes the Server Configuration Dialog
 */
void InitServerConfigurationDialog() {
    InitPortsTable();

    foreach (string key, ["AllowTcpForwarding", "X11Forwarding", "Compression"], {
	UI::ChangeWidget(`id(key), `Value, (Sshd::GetSSHDOption(key) == ["yes"]));
    });
}

/**
 * Initializes the Login Settings Dialog
 */
void InitLoginSettingsDialog() {
    UI::ChangeWidget( `id("MaxAuthTries"), `ValidChars, "0123456789");
    list <string> MaxAuthTries = Sshd::GetSSHDOption("MaxAuthTries");
    UI::ChangeWidget(`id("MaxAuthTries"), `Value, MaxAuthTries[0]:"0");

    foreach (string key, ["PrintMotd", "PermitRootLogin",
	"PasswordAuthentication", "RSAAuthentication", "PubkeyAuthentication"], {
	    UI::ChangeWidget(`id(key), `Value, (Sshd::GetSSHDOption(key) == ["yes"]));
    });
}

Functions InitServerConfigurationDialog() and InitLoginSettingsDialog() initialize the appropriate dialogs and fill them up with the current data.

Function InitLoginSettingsDialog() also sets the valid characters for the MaxAuthTries text entry to numbers only.

Function InitPortsTable() is called from the InitServerConfigurationDialog(). It sets the list of currently configured used ports into the table and enables Edit and Delete buttons when some ports are configured. In the case of no ports configured disables those two buttons.

2.9.2.5. Handling Add, Edit and Delete Buttons

/**
 * Removes the port from list of current ports
 *
 * @param string port_number
 */
void DeletePort (string port) {
    Sshd::SetSSHDOption("Port", filter (
	string single_port, Sshd::GetSSHDOption("Port"), ``(single_port != port)
    ));
}

/**
 * Function handles the adding or editing port number.
 * When the current_port is not 'nil', the dialog will
 * allow to edit it.
 *
 * @param string current_port a port number to be edited or 'nil' when adding a new one
 */
void AddEditPortDialog (string current_port) {
    UI::OpenDialog(`opt(`decorated), `VBox(
	`MinWidth (30,
	    `HBox(
		`HSpacing(1),
		`Frame(
		    (current_port == nil ?
		    /* A popup dialog caption */
		    _("Add New Port")
		    :
		    /* A popup dialog caption */
		    _("Edit Current Port")),
		    /* A text entry */
		    `TextEntry(`id("port_number"), _("&Port"), (current_port == nil ? "":current_port))
		),
		`HSpacing(1)
	    )
	),
	`VSpacing(1),
	`HBox(
	    `PushButton(`id(`ok), Label::OKButton()),
	    `HSpacing(1),
	    `PushButton(`id(`cancel), Label::CancelButton())
	)
    ));

    UI::ChangeWidget( `id("port_number"), `ValidChars, "0123456789");

    any ret = nil;
    while (true) {
	ret = UI::UserInput();
	if (ret == `ok) {
	    string new_port = (string) UI::QueryWidget(`id("port_number"), `Value);

	    if (new_port == "") {
		UI::SetFocus(`id("port_number"));
		Report::Error(_("Port number must not be empty."));
		continue;
	    }

	    Sshd::SetSSHDOption("Port", add (Sshd::GetSSHDOption("Port"), new_port));

	    if (current_port != nil) DeletePort(current_port);
	}

	break;
    }

    UI::CloseDialog();
}

/**
 * Function handles Add, Edit and Delete buttons
 *
 * @param any action from "add_port", "edit_port" or "delete_port"
 */
void HandleServerConfigurationDialog(any action) {
    string selected_port = (string) UI::QueryWidget(`id("Port"), `CurrentItem);

    // Adding a new port
    if (action == "add_port") {
	AddEditPortDialog(nil);
    // Editing current port
    } else if (action == "edit_port") {
	AddEditPortDialog(selected_port);
    // Deleting current port
    } else if (action == "delete_port") {
	if (Confirm::DeleteSelected()) DeletePort(selected_port);
    } else {
	y2error("Unknown action %1", action);
    }
      
    InitPortsTable();
}

The HandleServerConfigurationDialog() function handles the dialog events when any of Add, Edit or Delete buttons are pressed.

  • Add button - Calls the AddEditPortDialog() function with nil as the parameter which means that no port is going to be edited, just added. This AddEditPortDialog() function opens up a small pop-up window containing the Add New Port text entry and OK and Cancel buttons. When the OK button is pressed, the new port is added into the list of current ports and the dialog is closed.

  • Edit button - Behaves almost the same but it calls the same function with the current port as the parameter. The text entry name in the pop-up dialog is Edit Current Port then and when the OK button is pressed, it also removes the old port.

  • Delete button - Calls the standardized Confirm::DeleteSelected() function which should be called every time user tries to remove such entry from a table. Then, if user confirms the deleting, it calls DeletePort() function which deletes the selected port.

2.9.2.6. Capturing the Current Configuration from UI

/**
 * Stores the current configuration from Server Configuration Dialog
 */
void StoreServerConfigurationDialog() {
    Sshd::SetModified();

    // Stores all boolean values and turns them to the "yes"/"no" notation
    foreach (string key, ["AllowTcpForwarding", "X11Forwarding", "Compression"], {
    Sshd::SetSSHDOption(
    key,
    [ (((boolean) UI::QueryWidget(`id(key), `Value) == true) ? "yes":"no") ]
    );
    });
    }

/**
 * Stores the current configuration from Login Settings  Dialog
 */
void StoreLoginSettingsDialog() {
    Sshd::SetModified();

    // Stores an integer value as a string
    Sshd::SetSSHDOption(
	"MaxAuthTries",
	[ (string) UI::QueryWidget(`id("MaxAuthTries"), `Value) ]
    );

    // Stores all boolean values and turns them to the "yes"/"no" notation
    foreach (string key, ["PrintMotd", "PermitRootLogin",
	"PasswordAuthentication", "RSAAuthentication", "PubkeyAuthentication"], {
	    Sshd::SetSSHDOption(
		key,
		[ (((boolean) UI::QueryWidget(`id(key), `Value) == true) ? "yes":"no") ]
	    );
    });
}

These functions get the UI widgets statuses and store them using the Sshd::SetSSHDOption() function. You can see how the boolean value of every check box is read and transformed to the yes/no notation which is used in the configuration file. All options have to be lists of strings.

For reading the widget's status is used the UI::QueryWidget(`id(widget_id), `Value) function.

Widget with id MaxAuthTries has not a boolean value so has its own call of UI::QueryWidget() and Sshd::SetSSHDOption().

2.9.2.7. Standard End of File

}

This is the standard End of File left here just for the file completeness.

2.9.3. Adding the Complex Connection into the Dialogs

The module is nearly finished. The only thing we have to do is to add calling those Init*, Store* and Handle* functions into the dialogs definitions in the src/dialogs.ycp file.

This is the final content, added functions are explained below:

/**
 * File:	include/sshd/dialogs.ycp
 * Package:	Configuration of sshd
 * Summary:	Dialogs definitions
 * Authors:	John The Fish <john@thesmallfish.net>
 *
 * $Id: dialogs.ycp 13879 2004-02-05 11:29:30Z jtf $
 */

{

textdomain "sshd";

import "Label";
import "Wizard";
import "Sshd";

include "sshd/helps.ycp";

/**
 * Server Configuration Dialog
 *
 * @return any dialog result
 */
any ServerConfigurationDialog() {

    /* a dialog caption */
    string caption = _("SSHD Server Configuration");

    term contents = `VBox (
	`Left(`Label(_("SSHD TCP Ports"))),
	`Left(
	    `VBox (
		`MinSize( 40, 5,
		    /* A table header */
		    `Table(`id("Port"), `header("Port"), [])
		),
		`HBox (
		    /* a push button */
		    `PushButton(`id("add_port"), _("&Add...")),
		    /* a push button */
		    `PushButton(`id("edit_port"), _("&Edit...")),
		    /* a push button */
		    `PushButton(`id("delete_port"), _("&Delete"))
		),
		`VSpacing(1),
		`Frame (
		    /* a dialog frame caption */
		    _("Server Features"),
		    `VBox (
			/* a check box */
			`Left(`CheckBox(`id("AllowTcpForwarding"), _("Allow &TCP Forwarding"))),
			/* a check box */
			`Left(`CheckBox(`id("X11Forwarding"),      _("Allow &X11 Forwarding"))),
			/* a check box */
			`Left(`CheckBox(`id("Compression"),        _("Allow &Compression")))
		    )
		),
		`VStretch()
	    )
	)
    );

    Wizard::SetContentsButtons(caption, contents, HELPS["server_configuration"]:"",
	    Label::BackButton(), Label::NextButton());
    Wizard::DisableBackButton();

    InitServerConfigurationDialog();

    any ret = nil;
    while(true) {

	ret = UI::UserInput();

	/* abort? */
	if(ret == `abort) {
	    if(Sshd::Abort()) break;
	    else continue;
	/* next */
	} else if(ret == `next) {
	    StoreServerConfigurationDialog();
            break;
	/* add, edit or delete */
	} else if (ret == "add_port" || ret == "edit_port" || ret == "delete_port") {
	    HandleServerConfigurationDialog(ret);
	/* unknown */
        } else {
            y2error("unexpected retcode: %1", ret);
            continue;
        }
    }

    Wizard::RestoreBackButton();

    return ret;
}

/**
 * Login Settings Dialog
 *
 * @return any dialog result
 */
any LoginSettingsDialog() {

    /* a dialog caption */
    string caption = _("SSHD Server Login Settings");

    term contents = `VBox(
	`Frame (
	    _("General Login Settings"),
	    `VBox (
		/* A check box */
		`Left(`CheckBox(`id("PrintMotd"),            _("Print &Message of the Day After Login"))),
		/* A check box */
		`Left(`CheckBox(`id("PermitRootLogin"),      _("Permi&t Root Login")))
	    )
	),
	`VSpacing(1),
	`Frame (
	    _("Authentication Settings"),
	    `VBox (
		/* A text entry */
		`Left(`TextEntry(`id("MaxAuthTries"),           _("Ma&ximum Authentication Tries"))),
		/* A check box */
		`Left(`CheckBox (`id("PasswordAuthentication"), _("Pa&sswordAuthentication"))),
		/* A check box */
		`Left(`CheckBox (`id("RSAAuthentication"),      _("RSA Authenti&cation"))),
		/* A check box */
		`Left(`CheckBox (`id("PubkeyAuthentication"),   _("Public &Key Authentication")))
	    )
	),
	`VStretch()
    );

    Wizard::SetContentsButtons(caption, contents, HELPS["login_settings"]:"",
	    Label::BackButton(), Label::NextButton());
    Wizard::SetNextButton(`next, Label::AcceptButton());

    InitLoginSettingsDialog();

    any ret = nil;
    while(true) {

	ret = UI::UserInput();

	/* abort? */
	if(ret == `abort) {
	    if(Sshd::Abort()) break;
	    else continue;
	/* next */
	} else if(ret == `next) {
	    StoreLoginSettingsDialog();
            break;
	} else if(ret == `back) {
	    break;
	/* unknown */
        } else {
            y2error("unexpected retcode: %1", ret);
            continue;
        }
    }

    Wizard::RestoreNextButton();

    return ret;
}

}

2.9.3.1. Server Configuration Dialog

Into the ServerConfigurationDialog() function between the Wizard::DisableBackButton(); call and the while-loop add calling the dialog initialization function:

InitServerConfigurationDialog();

Into the ServerConfigurationDialog() function into the while-loop add the function call for storing configuration and for handling the Add, Edit and Delete buttons. The mentioned part of the while-loop should look like this one:

    while(true) {

    ret = UI::UserInput();
      
    /* abort? */
    if(ret == `abort) {
	if(Sshd::Abort()) break;
	else continue;
    /* next */
    } else if(ret == `next) {
	StoreServerConfigurationDialog();
	break;
    /* add, edit or delete */
    } else if (ret == "add_port" || ret == "edit_port" || ret == "delete_port") {
	HandleServerConfigurationDialog(ret);
    /* unknown */
    } else {
	y2error("unexpected retcode: %1", ret);
	continue;
    }
}

2.9.3.2. Login Settings Dialog

Into the LoginSettingsDialog() function between the Wizard::SetNextButton(`next, Label::AcceptButton()); call and the while-loop add calling the dialog initialization function:

InitLoginSettingsDialog();

Into the LoginSettingsDialog() function into the while-loop add the function call for storing configuration. It should look like this one:

    while(true) {

    ret = UI::UserInput();

    /* abort? */
    if(ret == `abort) {
	if(Sshd::Abort()) break;
	else continue;
    /* next */
    } else if(ret == `next) {
	StoreLoginSettingsDialog();
	break;
    } else if(ret == `back) {
	break;
    /* unknown */
    } else {
	y2error("unexpected retcode: %1", ret);
	continue;
    }
}

2.9.4. Checking the Progress

And that's it!

[Important]Important

Do not forget to run command sudo make install after you finish editing the files in the src/ directory to get the new versions installed.

Now you can run your new sshd module with command: /sbin/yast2 sshd.

2.10. Mission Completed

We have completed the project development, now it's time to install and run the application and send it to the openSUSE project.

You can download the current sources here.

2.10.1. Final Look & Feel

Let's enjoy the SSHD configuration module you have just done:

Try to change some values and check them in the /etc/ssh/sshd_config configuration file.

2.11. Conclusion

Congratulations! You have created you first own YaST module and you do understand all parts of it. You know...

2.11.1. YaST Knowledge Summary

When you known how to do this things, you might want to know where to find more information about YaST development and knowledge.

The YaST General Documentation is a top-most level of all the centralized documentation. Here are some important links chosen from that documentation:

2.11.2. Downloads

Here are listed the source codes in particular stages of the module development:

2.12. Testing and Tuning

...not finished...