Table of Contents
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.
Table of Contents
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.
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
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.
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
Table of Contents
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.
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.
Project specification is a very important step in module development. The simple example shows how the project specification could be written.
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.
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.
![]() | 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
This module will use only two dialogs arranged in a wizard sequence. Both dialogs will be based on the typical YaST dialog with a , , and 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
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.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:
Drawing of the second dialog:
The next step is to begin actual development.
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.
We have just created a new YaST sshd configuration module but how to start and run this module? The answer is pretty easy...
This is the way how to install the current state of the project into the system:
Enter the directory with source files: cd sshd
Check whether needed packages are installed and call YaST scripts needed for creating Makefiles: make -f Makefile.cvs
Check syntax, comments of functions, create generated documentation, compile *.ycp modules into their binary form *.ybc: make
Install all into particular directories, you have to become 'root' for that: sudo make install
![]() | 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.
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:
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.
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.
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?
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.
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.
The sshd.ycp
imports some binary modules such
as Progress, Report or
CommandLine, defines the command line options and
includes the wizards.ycp
file.
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.
After closing the UI, the control is returned back to the
sshd.ycp
client which finishes the operation and
exits.
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.
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.
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 |
---|---|
We have also other clients
|
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 |
---|---|
Never define the `back event, dialog wizard handles it by itself. |
![]() | Note |
---|---|
Please, take note, that sequences can call another ones:
Sequence called from a client should contain
Screenshot of the Wizard window: ![]() |
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 |
---|---|
Every string we want to mark for translation uses
the Translations insist on defined
|
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.
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 |
---|---|
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. |
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 |
---|---|
Our project also contains the |
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.
This part will summarize functions and files removed from our YaST module created using the y2tool helper.
You can download the changed source files here or you can download them one by one later in this section.
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
![]() | 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; }
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.
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.
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 |
---|---|
Structure of helps is: map HELPS = $[ // TRANSLATORS: Comment for translators (this is a help text for...) "help_for" : _("Help text marked for translation..."), ]; |
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.
This file has been completely removed from
the package and also the appropriate record in the
Makefile.am
file.
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
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.
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.
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 |
---|---|
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 |
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:
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.
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 |
---|---|
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 `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"))) ) ) |
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.
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(); }
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.
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.
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.
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.
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.
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.
Start the y2base binary /usr/lib/YaST2/bin/y2base stdio scr returns:
([])
Getting already configured options with command `Dir(.sshd) returns:
(["Port", "PasswordAuthentication", "UsePAM", "X11Forwarding", "AcceptEnv", "AcceptEnv"])
Reading the UsePAM option with command `Read(.sshd.UsePAM) returns:
(["yes"])
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"])
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.
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.
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.
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.
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.
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"));
/** * 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; } }
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.
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.
This part will show you how to connect the Sshd module data with the UI.
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; } }
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.
/** * 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.
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.
/** * 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.
/** * 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.
/** * 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.
/** * 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().
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; } }
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; } }
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; } }
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.
Congratulations! You have created you first own YaST module and you do understand all parts of it. You know...
how to create a new project from scratch.
how to handle user's events from the UI.
how to create your own SCR Agent.
how to run and test the SCR Agent directly.
how to initialize the UI state.
how to query and store the UI state.
what is supposed to be in each file of the project.
wow to use the SCR Agent in a YCP Module.
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:
Here are listed the source codes in particular stages of the module development:
Starting version—just a newly created module with the y2tool. Link to the respective part of the tutorial.
Implemented UI—after the UI is implemented and the dialog workflow is working. Link to the respective part of the tutorial.
Final version—when the module is completed. Link to the respective part of the tutorial.