This chapter takes module writers inside one of the standard Webmin modules, and explains which parts of its design they should copy.
Module design and CGI programs
Scheduled Cron Jobs module lets a user view, edit and create Cron jobs for all Unix users on a system. It gets the lists of jobs by reading several different files, such as those in the /var/spool/cron
directory, those in /etc/cron.d
and /etc/crontab.
The exact paths depend upon the operating system that Webmin is running on, as every Unix variant seems to have its own implementation of Cron.
As well as editing jobs, the module can also be used to execute those that have already been defined and view their output. Users can also edit the files that control which users have access to Cron, usually named /etc/cron.allow
and /etc/cron.deny.
The CGI programs that make up this module are:
index.cgi
Displays a list of jobs that the current Webmin user is allowed to access, each of which is a link to the editing page created by edit_cron.cgi
with a parameter identifying the index of the job to edit. The actual list comes from the list_cron_jobs
function in cron-lib.pl
.
edit_cron.cgi
Produces HTML for a form for either editing an existing job or creating a new one, depending on the idx
and new parameters. Again, the details of a job being edited are taken from the list_cron_jobs
function. At the bottom of the generated page are buttons that submit to either save_cron.cgi
or delete_cron.cgi
.
save_cron.cgi
Calls ReadParse
function to get the form inputs from edit_cron.cgi
, and validates them to make sure all of the required fields have been filled. If so, functions from cron-lib.pl
are called to either create a new job or update an existing, and then re-direct is called to make the user’s browser return to index.cgi
. But if an error is detected, the standard error function is called instead. When changing the user that a job runs as, this program needs to delete and re-create it so that it ends up in the right file, instead of just changing it in place.
delete_cron.cgi
Run when the Delete button on the editing form is clicked. Just calls a function from cron-lib.pl
to remove the job specified by the idx
parameter, and then redirects the browser to index.cgi
.
exec_cron.cgi
This CGI uses the safe_process_exec
function from the Running Processes module to run the command for a specified Cron job as the user who owns it, and display the output. It also deletes any environment variables that are specific to Webmin, so that programs run by the Cron job do not get confused and think that they are being called as CGI programs when this is not really the case.
edit_allow.cgi
Just displays a form for entering either a list of users who are allowed to use Cron, or a list of those who cannot. The current settings are obtained by calling functions in cron-lib.pl
.
save_allow.cgi
Saves the inputs from the form created by edit_allow.cgi
back to the original files, again by calling functions from the module’s library.
This module follows a design common to many others Webmin – a single page listing objects to edit, each of which is a link to a form for editing it. Your modules should use the same layout where appropriate, instead of displaying a huge table for editing multiple objects at once. It is a good idea to imitate this module’s use of multiple CGI programs as well, instead of trying to out everything in a single script. In all of the standard modules, each page is generated by a separate program, and if it is a form it is submitted to yet another program. This makes each simpler and easier to understand, instead of putting both the form generating and processing code into a single script. The redirect function is used by all of the save_
CGIs for form processing to return the user’s browser to the module’s main page, rather than to the editing form again.
The cron-lib.pl
library script
The real work in this module is done by the functions in cron-lib.pl
, which actually read and write the various Cron job files in their different formats. This is the way a Webmin module should be written, as it cleanly separates the user interface from the configuration file management. This prevents unnecessary duplication of code, and makes it easy to add support for some new Cron file one arises.
The functions in this library that CGI programs call are:
list_cron_jobs():
Returns an array of hash references, each of which contains the details of some Cron job. This information is actually read from several different files, and each job hash contains the name of the file that it came from in the file key, the position in that file in the line key and the and the format in the type key. This is used when the job is saved with change_cron_job
, so that it gets put back in the same place with the correct format. Many other Webmin modules store this kind of information in hashes that they create from configuration files, so that they know which part of the file to update.
create_cron_job( job ):
Takes a hash reference containing Cron job details, with the same keys as those returned by list_cron_jobs
. This is then converted to a correctly formatted line, and appended to a temporary copy of the user’s Cron jobs file. The copy_crontab
function is used to activate it, using the method explained below.
change_cron_job( job ):
Takes a hash reference returned by list_cron_jobs
but with some of the details updated, and converts it to a correctly formatted line of text. If it is a user’s personal Cron job then the line must be updated in a copy of the his jobs file. Otherwise, the original file that it came from can be updated directly.
delete_cron_job( job ):
Deletes the job passed in as a parameter by removing its line from the original file. If it was a user’s personal Cron job this is done in a temporary copy of his file instead of directly updating the original source.
list_allowed() and list_denied()
Return arrays of users who are allowed or not allowed to access Cron, respectively. These functions are primarily used by edit_allow.cgi
, and just read the contents of /etc/cron.allow
and /etc/cron.deny.
However, save_cron.cgi
also uses them to check if the user that you are creating a Cron job for can actually use it, as the crontab command will often fail if this is not the case.
save_allowed( user, … ) and save_denied( user, … )
These functions write the lists of users given as parameters to the /etc/cron.allow
or /etc/cron.deny
files, respectively. They are only used by save_allow.cgi
.
can_edit_user( access, user ):
This function is used to check if the current Webmin user can access the Cron jobs of a particular Unix user, based on the hash reference and username passed in as parameters. The reference is assumed to be the return value from get_module_acl
, which contains settings made in the Webmin Users module. Most of the CGI programs use it to limit their displays and prevent attempts to access jobs belonging to unauthorized users. If your module has access control features that can limit that objects that a user can access, a function like this is useful to prevent the duplication of code that checks ACL settings. Note that it is called in both edit_cron.cgi
and save_cron.cgi
, to block sneaky users who try to invoke the save program directly instead of going through the form.
show_times_input( job ):
This code prints HTML for the part of a form for editing the times at which a Cron job is run. It used to be in edit_cron.cgi
, but was moved into the library so that other modules which set up Cron jobs (such as Filesystem Backup) can make use of the same inputs in their user interface.
parse_times_input( job, in ):
This function parses the inputs from the form created by show_times_input
. Again, it is used by other modules as well as in save_cron.cgi
.
You might wonder, why do some of the functions above update a temporary file instead of directly editing the files in /var/spool/cron
that contain user Cron jobs? The reason is that the crontab command must be used to install a modified file for the Cron daemon to notice the change and for it to take effect. This is done by the copy_crontab
function, which invokes the appropriate crontab command for the operating system. Normally when crontab is run by a user, it starts an editor like vi
for the user to edit a temporary copy of the file, which is when moved back into /var/spool/cron.
However, this module sets the EDITOR
environment variable to the cron_editor.pl
script which just copies the temporary file created by the module over the file passed to the script for editing by crontab. When it exists, the changes made by the module are properly installed and the temporary file can be deleted.
This process is not necessary for Cron jobs in /etc/crontab
or /etc/cron.d
though, as the Cron daemon automatically detects when those files have been updated. For this reason the change_cron_job
and delete_cron_job
functions can edit them directly.
Because Cron is a great tool for running scripts on a regular basic, several other modules make use of this one to set up jobs of their own. For example, Webmin Configuration uses it to schedule the automatic download of updated modules, Webalizer Logfile Analysis uses it to have logs processed regularly, and System and Server Status uses it to set up scheduled monitoring.
All of this is done by making foreign calls to the cron module. If your module needs to do the same, it is advisable to make use of the code in cron-lib.pl
that already supports a wide variety of operating systems and creates jobs in the correct.
Module configuration settings
This module demonstrates how the various Cron file locations, formats and programs on different operating systems can be supported by the same code. If you look in its directory, you will see numerous files with names starting with config-
, such as config-solaris
and config-redhat-linux
. Each specifies the files to read and commands to use for a particular operating systems. The code in cron-lib.pl
makes numerous references to %config
when listing and updating jobs, which of course is filled with the contents of /etc/webmin/cron/config.
This file in turn comes from the appropriate config-
file in the module’s directory, chosen at the time Webmin was installed.
If your module manages some service that differs slightly between operating systems, this method of using different default configurations makes sense. It can also be useful when writing a module for some server like Apache for the default configuration and program file paths will differ depending on the operating system or Linux distribution, due to the vast number of different Apache packages out there.
The file config.info
in this module defines inputs for editing both the operating system dependant options in the configuration file, and those related only to the module’s user interface. Sometimes it makes very little sense to let users edit such settings as the location of users’ personal Cron job files, as they are pretty much determined by the operating system in use. For this reason, you might think that taking those fields out of config.info
is a good idea, so that users cannot mess up the module’s configuration.
This will work fine, as it is really the entries in the appropriate config-
file that gets (indirectly) loaded into the %config
hash. The config.info
file just controls which ones are editable and what values are allowed – any others will be left unchanged when the user clicks on Module Config. However, in the Scheduled Cron Jobs module all configuration settings can be edited, just in case the user upgrades the version of Cron that comes with his operating system to some totally different package.
The lang internationization directory
Thanks to the generous contributions of Webmin users, the lang subdirectory for this module contains files for several different languages. The setting in the Language form of the Webmin Configuration module determines which one is loaded into the %text
hash when init_config
is called, as explained earlier.
This module uses no hard-coded text strings in any of its CGI programs or other scripts. Instead, references to an appropriate message for the current language like $text{'index_create'}
or &text('exec_cmd')
are used. If your module might ever be translated into a different language, you should do the same in its CGI programs as well. Even though it is slightly more work to put messages into a separate file, it is worth it in the long run.
The acl_security.pl
access control script
The Webmin Users module can be used to configure detailed access control settings for a particular user and module. The actual form for editing these settings is generate by the acl_security.pl
script in the module’s directory. Because this module lets an admin define which Unix users a particular Webmin user can edit Cron jobs for, it has one of these scripts as well.
As you can see by opening the file in an editor, it contains the required acl_security_form
and acl_security_save
functions. The first prints HTML for form inputs within a 4-column table, with their current settings based on the contents of the hash reference passed in as a parameter. The second checks the values in %in
and uses them to fill in the hash reference from its parameter, which upon exiting is saved by the Webmin Users module back to /etc/webmin/cron/username.acl
.
The ACL settings for this module let the administrator choose allowed Unix users by several different means. He can either grant access to all of them, to just the one whose name matches the current Webmin user, to a specific list of users, to users with some primary group or to users with UIDs within some range. Many other modules have similar options to specifying allowed users of some kind. If your module deals with some kind of Unix user-related configuration, its acl_security.pl
script should have similar inputs.
On many systems (such as those used for virtual hosting), a single sub-administrator may be responsible for many Unix accounts, possibly those with a certain primary group or with UIDs within some fixed range. This kind of access control makes it possible to safely give such as sub-admin a Webmin login to manage only those specific Unix users.
All of the CGI programs in this module use the get_module_acl
standard function to get the access control settings for the current Webmin user. The return value is generally stored in the %access
hash, which is consulted to determine if the Webmin user can access Cron jobs for a particular Unix user. This is mostly done by called can_edit_user (explained above), and then calling error if access was denied.
Code in your module should do the same, and every CGI program should check to make sure that it is not being accessed inappropriately. One change that you might want to make is to put the call to get_module_acl
into your module’s library script so that the %access
hash is available globally to every CGI program, instead of each of them having to call it explicitly.
When creating a module that can be set up to allow limited access like this, you must be very careful to stop the user from escaping its restrictions in any way. This means following all of the normal rules about programming CGI scripts, such as not passing user inputs directly to the system or open functions. Because Webmin modules are normally accessed by a user who has full root privileges, security holes like this would usually not matter. However, when the user has been given less privileges through the user of module access control, a bug could let him executed arbitrary commands or edit files as root.
The log_parser.pl
log reporting script
Like all good Webmin modules, this one logs actions taken by users to that they can be viewer later in the Webmin Actions Log module. The save_cron.cgi
, delete_cron.cgi
, save_allow.cgi
and exec_cron.cgi
programs all call the standard webmin_log
function with parameters indicating what action has just taken place, for this information is then written to a log file for later reporting.
Even though just about any arguments can be passed to the webmin_log
function, it is usually a good idea to follow the standard that this and other modules use. The first action parameter should be the action performed, such as save or delete. The type parameter should be the kind of object the action applies to, such as cron or user. The object parameter should be the name of the object effected, such as fred
or www.foo.com
. Finally, the params parameter must be a hash reference containing additional information about the action, such as the structure of the object being modified or the contents of %in
. All parameters except action are optional, so it is quite reasonable and common for a module to use code like &webmin_log("stop")
.
In addition, all of these programs make use of the lock_file
and unlock_file
functions to obtain locks on files that they change. This causes the actual changes to the Cron files to be captured for inclusion in the log as well, so that inexperienced administrators can see exactly what the module has been doing. Your module should make use of these functions as well, especially those for locking. They protect critical files from simultaneous, and give you detailed file change logs for free if you decide to add calls to action_log
as well.
The other side of logging is the conversion of the logged parameters into human-readable form, which is done by the log_parser.pl
script. If you view the code for this module, you will see that it simply uses the parameters to decide what to pass to text, and returns the resulting string. Note that the html_escape
function is used to remove any special HTML characters from Cron commands, which may otherwise cause invalid HTML to be included in the log search results. If your module includes a log_parser.pl
script which might return text containing characters like <
, >
or &
, be sure to call html_escape
on the appropriate parts.
Unlike the parse_webmin_log
function is most other modules, the one in the Scheduled Cron Jobs module checks the long parameter to decide if a long or short action description should be output. The long form includes the actual command in the Cron job, which will only fit on the page displaying details of a single log entry in the Webmin Actions Log module. However, in most modules the message is always short enough to completely ignore this parameter.
If a parameter to webmin_log
was omitted or set to undef
by the CGI program that created it, the actual value passed to parse_webmin_log
will be a single dash instead. This happens because a –
is used in the log file to represent a missing parameter.
The useradmin_update.pl
user synchronization script
Other Webmin modules can choose to be notified when a user is created, modified or deleted in the Users and Groups module. This is normally used to keep some other user list in sync (such as the Samba password file), but can be handy for other purposes as well.
The Scheduled Cron Jobs module has a useradmin_update.pl
script so that it can detect the renaming and deletion of users, and update their Cron job files respectively. Normally when a Unix user is removed his Cron jobs will continue to exist, even though they will no longer work. And if a user is renamed, his jobs will still be listed under the old name, which will prevent them from working properly.
To avoid the first problem, the useradmin_delete_user
function removes the personal Cron jobs file for any Unix user who is being deleted. The useradmin_modify_user
function checks to see if the user has been renamed, and if so renames both the user’s personal Cron file and any jobs in other files as well. Any other changes to the user are ignored, as they are not relevant to this module.
Those few modules that make use of a useradmin_update.pl
script will probably have it perform different tasks to this module’s. See the script in the samba directory for an example of how to synchronize a separate password file instead. If your module’s script does do something similar, it should include options somewhere (perhaps on the Module Config page) to turn synchronization on or off. Any such options should be off by default, so that other configuration files are not unexpectedly updated when the user is managing Unix users.