]> YaST Tutorials 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. Lukas Ocilka Chapter in general. Klara Cihlarova Chapter structural correctness, logic checks, ideas and enhancements. YaST Development in General 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. 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: 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. 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 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. Tutorial 1—Simple YaST Module 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. 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. 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. 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. 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. General Project Definition 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 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. 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: Starting Up The next step is to begin actual development. 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 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. 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... Installing Project on the System 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 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. 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: Watching YaST Logs 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 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. 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. 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? 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. 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. File <filename>sshd.desktop</filename> Application definitions for KDE, AutoYaST, YaST Control Center... 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. File <filename>sshd.ycp</filename> 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 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. 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. File <filename>wizards.ycp</filename> Contains dialogs sequence definitions that are used by clients. Wizards are stored under the /usr/share/YaST2/include/<module-name>/ path. 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. Never define the `back event, dialog wizard handles it by itself. 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: File <filename>dialogs.ycp</filename> Dialogs definitions and their simple handling. Dialogs are stored under the /usr/share/YaST2/include/<module-name>/ path 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. 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. File <filename>complex.ycp</filename> YCP include which contains complex functions for dialogs handling. 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. File <filename>helps.ycp</filename> Help texts for every dialog. Helps are stored under the /usr/share/YaST2/include/<module-name>/ path 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. 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. File <filename>Sshd.ycp</filename> 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. 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. 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. 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. Modifying Source Files This part will summarize functions and files removed from our YaST module created using the y2tool helper. Changes in the <filename>src/</filename> directory: You can download the changed source files here or you can download them one by one later in this section. Files <filename>Sshd2.pm</filename> and <filename>Makefile.am</filename> 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 File <filename>Sshd.ycp</filename> 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; } File <filename>complex.ycp</filename> 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. File <filename>dialogs.ycp</filename> 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. 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. This simple dialog contains two frames of General Login Settings and Authentication Settings with check boxes.   File <filename>helps.ycp</filename> 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>"), Structure of helps is: map HELPS = $[ // TRANSLATORS: Comment for translators (this is a help text for...) "help_for" : _("Help text marked for translation..."), ]; File <filename>sshd.ycp</filename> 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. File <filename>sshd_auto.ycp</filename> This file has been completely removed from the package and also the appropriate record in the Makefile.am file.   File <filename>sshd_proposal.ycp</filename> 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   File <filename>wizards.ycp</filename> 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. Changes in the <filename>testsuite/</filename> 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. 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: 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 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: 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. 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. 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"))) ) ) 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. 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(); } 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. 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. 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. 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. 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: 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. 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. 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"]) 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. 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. 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. 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. 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. 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")); Complete Sshd.ycp file 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. 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. Connecting the Configuration Data with the UI This part will show you how to connect the Sshd module data with the UI. 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: 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: Functionality contained in this file is explained in parts. 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. 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. 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. 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. 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. 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(). Standard End of File } This is the standard End of File left here just for the file completeness. 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: 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; } } 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; } } Checking the Progress And that's it! 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. 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. 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. Conclusion 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. 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: FAQ—Frequently Asked Question YCP Data Types YCP Operators YCP Program Structure YCP Modules YaST Logging UI functions and Widgets Downloads 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. Testing and Tuning ...not finished...