A module should be written to administer one service or server, such as the Unix password file or the Apache web server. Some complex system functions may even be split over several modules - for example, disk partitioning, mounting disks and disk quota management are 3 separate modules in the standard Webmin distribution.
Modules can theoretically be written in any language. However, to make use of the Webmin API Perl version 5.002 or above should be used. A module should be written entirely in Perl, with no C functions or external binary programs. The aim is for modules to be as portable as possible across different Unix systems and CPU types.
For a module to be displayed on the main Webmin menu, it must contain at least the following two files.
When you first create a new module, it will not be in the ACL of any Webmin user and so you will not be able to see it in the main menu. To make your module visible, you must either edit the file /etc/webmin/webmin.acl, or use the Webmin Users module to grant yourself access.
Assuming your module is being written in perl, you should begin by writing a perl script that contains functions used by the CGI programs in your module. This script is usually called something like lib.pl or foobar-lib.pl. A minimal example of such a script might look like :
# foobar-lib.pl # Common functions used for managing the foobar user list do '../web-lib.pl'; (1) &init_config(); (2) # list_users() (3) # Returns a list of all foobar users sub list_users { ... } |
The 3 important features of the example above are :
In Webmin versions 0.92 and above, you can specify multiple link
and text parameters to have the function generate multiple
footer links, like so :
&footer("", "module index", "list.cgi", "users list");
$whatfailed = "Failed to save user"; if (!$in{'name'}) { &error("Missing username"); } if ($in{'name'} =~ /a/) { &error("'$in{'name'}' is not a valid username"); }
&foreign_require("proc", "proc-lib.pl"); @procs = &foreign_call("proc", "list_processes"); &foreign_call("proc", "renice_proc", $pid, -10);The example above calls the proc module to get a list of processes, and then again to change the priority of some process.
In Webmin versions 0.960 and above, you can use the normal Perl syntax for calling functions in other modules, like :
&foreign_require("proc", "proc-lib.pl"); @procs = &proc::list_processes(); &proc::renice_proc($pid, -10);
The optional error and callback functions are only supported in Webmin versions 0.93 and above. If error is supplied, it must be a reference to a scalar that will be set with an error message if the download fails, instead of the function simply calling the standard error function in the case of a failure.
The callback parameter can be a reference to a function that will be called back to at various stages of the download process. When called, the first parameter indicates the status, and the second some additional information. Possible status codes are :
The optional error and callback parameters are only supported in Webmin versions 0.93 and above, and behave in exactly the same way as in the http_download function, documented above.
Before calling functions from a module on another server with remote_foreign_call, you must use this function to bring in the appropriate library. The server parameter is the hostname of the remote Webmin server, the module parameter is the name of the module you want to call functions in, and the file parameter the name of a library file in that module directory.
&remote_foreign_require("www.blah.com", "apache", "apache-lib.pl"); @servers = &remote_foreign_call("www.blah.com", "apache", "get_config");As the example shows, the remote_foreign_call function returns whatever is returned by the function on the remote server.
&header("The Foo Module", undef, undef, 1, 1, undef, &help_search_link("foo", "man", "doc", "google"));
The returned session handle should be used with the read_http_connection and write_http_connection functions to send any additional headers and to read back the response headers and body. When done, the close_http_connection function should be called with the session handle.
In addition to switching the UID, this function also sets the global variable @remote_user_info to the details of the Unix user, as returned by the getpwnam Perl function.
The files defaultuconfig in the module's directory, uconfig in the module configuration directory under /etc/webmin and config in the user's module configuration directory will be read in that order into the global hash %userconfig. See the Usermin section of this documentation for more details.
You can see examples of these functions in action by downloading and installing the javascript-lib.pl samples webmin module, which shows what each function does.
- Parameters
- &jroll_over("url", "name", "border", "imgoff", "imgon");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &jroll_over("http://www.mscsoftware.com", "mscbutton", "0", "images/msc_off.gif", "images/msc_on.gif");
- Parameters
- &jimg_preload("image", "image", "image", "etc");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &jimg_preload("image/1.gifi", "imgage/u.gif", "images2/button.jpg", "images2/logo.png");
- Parameters
- &jimg_update("name", "image");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &jimg_update("mscimage", "images/new_logo.jpg");
- Parameters
- &janim("name", "speed", "image", "image", "image", "etc")
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &janim("mscanim1", "1250", "images/1.gif", "images/slide.png", "images/logofin.jpg")
- Parameters
- &janim_start("name");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &janim_start("mscanim1");
- Parameters
- &janim_stop("name");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &janim_stop("mscanim1");
- Parameters
- &jalert("message");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &jalert("Your alert message!");
- Parameters
- &jwindow("url", "name", "width", "height");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &jwindow("http://www.msclinux.com", "msclinux", "800", "600");
- Parameters
- &jwindow_xy("name", "x", "y");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &jwindow_xy("msclinux", "250", "120");
- Parameters
- &jterminal("name", "width", "height");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &jterminal("mscterm", "100", "300");
- Parameters
- &jwrite("name", "data");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &jwrite("mscterm", "Download MSC.Linux from http://www.msclinux.com");
- Parameters
- &jtext("text", "offcolor", "oncolor");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &jtext("MSC.Linux Rules!", "red", "#F1F1F1");
- Parameters
- &jtalkback();
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &header($text{'index_title'}, "", undef, 1, 1);
- &jtalkback();
- Parameters
- &jerror("title", "email", "width", "height", "errmsg", "url", "erroredonline");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &header($text{'index_title'}, "", undef, 1, 1);
- &jtalkback();
- @somedata = `cat msclinux.log` or &jerror("MSC.Linux Bug Report", \
- "john.smoth\@some.email.com", "400", "300", $!, $0, __LINE__);
- Parameters
- &jtalkback_link("title", "email", "width", "height", "text", "type");
- Code Example
- #!/usr/bin/perl
- require '../javascript-lib.pl';
- &header($text{'index_title'}, "", undef, 1, 1);
- &jtalkback();
- &jtalkback_link("Bug Report Window", "totheadmin\@help.desk.com", 400, 300, "Text Link", 0);
- &jtalkback_link("Bug Report Window", "totheadmin\@help.desk.com", 400, 300, "Button", 1);
The associative array %gconfig contains global configuration options, typically from the file /etc/webmin/config. Some useful global configuration options are :
Configuration parameters can also be used for options that the user may want to occasionally change. For example, the BIND module has a parameter that controls for format of new DNS zone serial numbers. When the 4th parameter of the header() function is set, a link will be generated to a CGI program that allows the user to edit the configuration of the current module. This program reads the file config.info in the module directory to determine the possible values for each config parameter. A typical config.info file might look like :
foobar_path=Path to foobar config file,0 display_mode=Index page display mode,1,0-Long,1-Medium,2-Short password_file=Foobar server users file,3,None file_user=Config files are owned by user,5Each line is in the format
config_name=description,type[,values]
The meanings of the parts of each line are :
When a module is installed (either as part of a Webmin distribution or separately) a config file appropriate to the OS being used is copied from the module directory to the configuration directory (usually under /etc/webmin). To decide which base config file to use, Webmin uses the OS name and version chosen when setup.sh was run to look for the following files
config-osname-osversion
config-osname
config
Where osname is something like redhat-linux or solaris, and osversion is something like 2.6 or 5.0. A typical module might have the following config files
config-redhat-linux
config-redhat-linux-5.0
config-slackware-linux
config-debian-linux
config-solaris
Webmin treats each of the Linux distributions as a different OS, as each has different locations for things like the Apache config file, crontab files and bootup scripts. The OS version number for Linux should be the distribution version (such as 4.1 or 5.0) rather than the kernel version.
print "<table border width=100%>\n"; print "<tr $tb> <td><b>Foo</b></td> <td><b>Bar</b></td> </tr>\n"; print "<tr $cb> <td>some value</td> <td>another value</td> </tr>\n"; ...
No module should ever corrupt a service config file or remove options that it does not understand. Modules should be able to parse any valid configuration without requiring special comments or a special format. If your module cannot deal with some option in a config file, it should be left alone.
Webmin modules should be designed to be easy for novices to use, but still allow the user to do almost everything that could be done by editing the config file directly.
print &hlink("Enter username:", "name"), "<input name=username size=20><p>\n";would output a link to display the help page help/name.html.
Module ACL options are set in the Webmin Users module by clicking on the name of a module next to a user's name. The options available are generated by code from the module itself (except for the Can edit module configuration? option, which is always present). When the user clicks on Update the form parameters are also parsed by code from the module being configured, before being saved in the Webmin config directory.
A module wanting ACL options must contain a file called acl_security.pl in its directory. This file must contain two perl functions :
An example acl_security.pl might look like :
do 'foo-lib.pl'; sub acl_security_form { print "<tr> <td><b>Can edit users?</b></td>\n"; printf "<td><input type=radio name=edit value=1 %s> Yes\n", $_[0]->{'edit'} ? 'checked' : ''; printf "<input type=radio name=edit value=0 %s> No</td> </tr>\n", $_[0]->{'edit'} ? '' : 'checked'; } sub acl_security_save { $_[0]->{'edit'} = $in{'edit'}; }If a user has not yet had any ACL options set for a module, a default set of options will be used. These are read from the file defaultacl in the module directory, which must contain name=value pairs one per line. These options should allow the user to do anything, so that the admin or master Webmin user is not restricted by default.
To actually enforced the chosen ACL options for each user, your module programs must use the get_module_acl function to get the ACL for the current users, and then verify that each action is allowed. When called with no parameters this function will return a hash containing the options set for the current user in the current module, which is almost always what you want. For example :
#!/usr/local/bin/perl require 'foo-lib.pl'; %access = &get_module_acl(); $access{'create'} || &error("You are not allowed to create new foo users");
When designing a module that some users will have limited access to, remember the user can enter any URL, not just those that you link to. For example, just doing ACL checking in the program that displays a form is not enough - the program that processing the form should do all the same checks as well. Similarly, CGI parameters should never be trusted, especially hidden parameters.
To have your module notified when a user is added, updated or deleted you must create a perl script called useradmin_update.pl in your module directory. This file must contain three perl functions :
user | The login name of the new or modified user |
uid | The Unix UID of the user |
gid | The Unix GID for the user's primary group |
pass | The user's password, encrypted with the crypt() function |
plainpass | The user's password in plain text. This is only available when the passmode key is equal to 3 |
passmode | This number depends on the choice made for the Password field
in the Create User or Edit User form.
|
real | The user's real name |
home | The user's home directory |
shell | The user's login shell |
If the system has shadow passwords enabled, other keys may also be available - but it is not a good idea to rely on them.
When your functions are called, they will be in the context of your module. This means that your useradmin_update.pl script can require the file of common functions used by other CGI programs. The functions can perform any action you like in order to update other config files or whatever, but should not generate any output on STDOUT, or take too long to execute.
A partial example useradmin_update.pl might look like :
do 'foo-lib.pl'; sub useradmin_create_user { local $lref = &read_file_lines($users_file); push(@$lref, "$_[0]->{'user'}:$_[0]->{'pass'}"); &flush_file_lines(); }
message_code=Message in this language
The default language for Webmin is english (code en), so every module should have at least a file called lang/en. If any other language is missing a message, the english one will be used instead. Check the file lang_list.txt for all the languages currently supported and their codes. To change the current language, go into the Webmin Configuration module and click on the Language icon.
When your module calls the init_config function, all the messages from the appropriate translation file will be read into the hash %text. Thus instead of generating hard-coded text like this :
print "Click here to start the server :<p>\n";
Your module should use the %text hash like this :
print $text{'startmsg'},"<p>\n";
Messages from the appropriate file in the top-level lang directory are also included in %text. Several useful messages such as save, delete and create are thus available to every module.
In some cases, you may want to include some variable text in a message. Because the position of the variable may differ depending on the language used, message strings can include placemarkers like $1, $2 or $3. The function text should be used to replace these placemarkers with actual values like so :
print &text('servercount', $count),"<p>\n";
Your module's module.info file can also support multiple languages by adding a line like desc_XX=module description for each language, where XX is the language code. You can also have a separate config.info file for each language, called config.info.XX where XX is the language code, and separate help files for each language, named like intro.XX.html. In all cases, if there is no translation for the user's chosen language then the default (english) will be used instead.
You don't have to finish translating the entire of Webmin for your translation to be useful. Many people just do a few modules at a time, or skip the help pages initially.
When your translation has made some progress, send me a .tar.gz file of
all the translated files at jcameron@webmin.com so that I can include it in the
main Webmin distribution. The best way to tar up everything is with the
command :
tar czf /tmp/translation.tar.gz */lang/sw lang/sw */module.info */config.info.sw */help/*.sw.html
Translators may also find the follow programs usedful :
drwxrwxr-x 3001/10 0 Aug 12 21:53 1998 dfsadmin/ drwxrwxr-x 3001/10 0 Nov 7 01:10 1997 dfsadmin/images/ -rw-rw-r-- 3001/10 245 Aug 1 23:41 1998 dfsadmin/images/icon.gif -rw-rw-r-- 3001/10 1438 Aug 1 23:41 1998 dfsadmin/images/dfsadmin.gif -rw-rw-r-- 3001/10 1541 Aug 1 23:41 1998 dfsadmin/images/create_share.gif -rw-rw-r-- 3001/10 1265 Aug 1 23:41 1998 dfsadmin/images/edit_share.gif drwxrwxr-x 3001/10 0 May 16 18:32 1997 dfsadmin/test/ -rw-r--r-- 3001/10 493 May 16 18:32 1997 dfsadmin/test/dfstab -rw-r--r-- 3001/10 483 May 16 18:15 1997 dfsadmin/test/dfstab.bak -rw-rw-r-- 3001/10 2774 Jul 29 22:22 1998 dfsadmin/dfs-lib.pl -rwxr-xr-x 3001/10 1582 Mar 31 15:45 1998 dfsadmin/index.cgi.bak -rw-rw-r-- 3001/10 49 Aug 12 21:53 1998 dfsadmin/module.info -rwxr-xr-x 3001/10 1596 Mar 31 15:45 1998 dfsadmin/index.cgi -rw-rw-r-- 3001/10 2775 Jul 29 22:22 1998 dfsadmin/dfs-lib.pl.bak -rw-rw-r-- 3001/10 199 Mar 5 19:30 1998 dfsadmin/help.html -rw-rw-r-- 3001/10 175 Mar 5 19:30 1998 dfsadmin/config.info -rw-r--r-- 3001/10 140 Mar 5 19:30 1998 dfsadmin/config-solaris -rwxr-xr-x 3001/10 142 Mar 5 19:30 1998 dfsadmin/delete_share.cgi -rwxr-xr-x 3001/10 4842 Mar 5 19:30 1998 dfsadmin/edit_share.cgi -rwxr-xr-x 3001/10 657 Jun 8 15:02 1998 dfsadmin/restart_sharing.cgi.bak -rwxr-xr-x 3001/10 3000 Mar 5 19:30 1998 dfsadmin/save_share.cgi -rw-rw-r-- 3001/10 57 Aug 12 21:53 1998 dfsadmin/module.info.bak -rwxr-xr-x 3001/10 573 Jun 8 15:02 1998 dfsadmin/restart_sharing.cgiThe standard extension for Webmin modules is .wbm, but any filename can be used.
Webmin modules can also be packaged as RPMs, which are suitable for installing on servers on which the RPM version of Webmin itself is already installed. I have created a script called makemodulerpm.pl that can package up an module directory into an RPM by creating the spec file automatically.
Locking is done by the function lock_file, which takes the name of a file as a parameter and obtains and exclusive lock on that file by creating a file with the same name but with .lock appended. Similarly, the function unlock_file removes the lock on the file given as a parameter. Because the .lock file stores the PID of the process that locked the file, any locks a CGI program holds will be automatically released when it exits. However, it is recommended that locks be properly released by calling unlock_file or unlock_all_files before exiting.
The following code shows how the locking functions might be used :
&lock_file("/etc/something.conf"); open(CONF, ">>/etc/something.conf"); print CONF "some new directive\n"; close(CONF); &unlock_file("/etc/something.conf");Locking should be done as soon as possible in the CGI program, ideally before reading the file to be changed and definately before writing to it. Files can and should be locked during creation and deletion as well, as should directories and symbolic links before creation or removal. While this is not really necessary to prevent file corruption, it does make the logging of file changes performed by the program more complete, as explained below.
The function webmin_log should be called by CGI programs after they have successfully completed all processing and file updates. The parameters taken by the function are :
For example, a module might call the function like this :
&lock_file("/etc/foo.users"); open(USERS, ">>/etc/foo.users"); print USERS "$in{'username'} $in{'password'}\n"; close(USERS); &unlock_file("/etc/foo.users"); &webmin_log("create", "user", $in{'username'}, \%in);Because the raw logfiles are not easy to understand, Webmin also provides support for converting detailed action logs into human-readable format. The Webmin Actions Log module makes use of a Perl function in the file log_parser.pl in each module subdirectory to convert logs records from that module into a readable message.
The file must contain the function parse_webmin_log, which is called once for each log record for this module. It will be called with the following parameters :
require 'foo-lib.pl'; sub parse_webmin_log { local ($user, $script, $action, $type, $object, $params, $long) = @_; if ($action eq 'create') { return &text('log_create', $user); } elsif ($action eq 'delete') { return &text('log_delete', $user); } }Because the log_parser.pl file is read and executed in a similar way to how the acl_security.pl file is handled by the Webmin Users module, it can require the module's own library of functions just like any module CGI program would. This means that the &text function and %text hash are available for accessing the module's translated text strings, as in the example above.
Webmin can also be configured to log exactly what file changes have been made by each CGI program before calling webmin_log. Under Logging in Webmin Configuration there is an option marked Log changes made to files by each action which when enabled will cause the webmin_log function to use the diff command to find changes made to any file locked by each program.
When logging of file changes is enabled, the Action Details page in the actions log module will show the diffs for all files updates, creations and deletions by the chosen action. If locking of directories and symbolic links is done as well, it will show their creation and modification as well.
As well as having their file changes logged, programs can also use the common functions system_logged, kill_logged and rename_logged which take the same parameters as the Perl system, kill and rename functions, but also record the event for viewing on the Action Details page. There is also a backquote_logged function which works similar to the Perl backquote operator (it takes a command an executes it, returning the output), but also logs the command. If these functions are used they must be called before webmin_log for the logging to be actually recorded, as in this example :
if ($pid) { &kill_logged('TERM', $pid); } else { &system_logged("/etc/init.d/foo stop"); } &webmin_log("stop");
require 'yourmodule-lib.pl'; sub module_install { system("cp $module_root_directory/somefile $config_directory/somefile") if (!-r "$config_directory/somefile"); }The function will be called when a module is installed from the Webmin Configuration or Cluster Webmin Servers modules. However, it is not called if the install-module.pl script is used.
Similarly, if your module contains a file called uninstall.pl, the perl function module_uninstall in that file will be called just before the module is deleted. This can happen when it is deleted using the Webmin Users or Cluster Webmin Servers modules, or when the entire of Webmin is uninstalled. The uninstall function should clean up any configuration that will no longer work when the module is uninstalled, such as cron jobs that reference scripts in the module.
Like a module, a theme is a directory under the Webmin root directory that contains certain files. The most important is the theme.info file, which has the same format as the module.info file - name=value entries, one per line. Required names and their values are:
A theme can also contain a config file, also in name=value format. The values defined in this file control the behaviour of the standard header and footer functions. Supported names and their values are:
In addition to changing the default colours, a theme can be used to selectively override any icon or CGI program used by Webmin. When a theme is chosen, its directory becomes an 'overlay' root directory for the Webmin webserver. Thus, if your theme subdirectory contains a file called images/newlogo.gif, it will replace the logo on the main menu when it is displayed, because the webserver will look in the theme directory first before looking for images/newlogo.gif under the top-level directory.
In this way any of the module icons can be overridden, as can the images used to make up the titles at the top of pages. For example, if your theme directory contained a file called useradmin/images/icon.gif, it would be used as the icon for the Users and Groups module on the main menu. Because this 'replacement' does not actually change the real images, the user can switch between themes or back to the default theme easily.
CGI programs can also be overriden as well, in exactly the same way. This can be used to do things like changing the way the main menu is displayed, by putting a custom index.cgi script in your theme directory. However, this ability should be used carefully as changes to the real CGIs may break your custom script if its behaviour is different to the one it replaces. Also, note that when a theme CGI is executed, it will be in the real directory and not the theme subdirectory. For example, this means that a custom top-level index.cgi script would have to require ./web-lib.pl instead of ../web-lib.pl, just as the real index.cgi does.
In Webmin versions 0.92 and above, a theme can override some of the common HTML-generating functions by adding a line like functions=theme.pl to the config file and creating a theme.pl script in the theme's directory containing one or more of the following functions :
The final part of creating a theme is packaging it. Just like module, themes are packaged as a tar file of the theme directory, usually with a .wbt extension. They can then be installed through the Webmin Themes page.
By default, module CGI programs are run as root, just like in Webmin. This is necessary because some tasks (like changing passwords) can only be done as root. However, most Usermin modules do not need superuser privileges and so should call the standard switch_to_remote_user function just after calling init_config , in order to lower privileges to those of the logged-in user.
Usermin module can have global configuration variables that are initially set from the config or config-ostype file in the module directory, and are available in %config. However, these variables are never editable by the user - they can only be set in the Usermin Configuration module of Webmin.
Per-user configurable options are supported though, using a different mechanism. When the standard create_user_config_dirs function is called, the global hash %userconfig will be filled with values from the following sources, with later sources overriding earlier ones :
If you create your own Usermin module, it should be packaged in exactly the same way as a Webmin module (as a .tar or .tar.gz file). However, the module.info file must contain the line usermin=1 so that it cannot be installed into Webmin where it would not work properly.
A Python implementation of the Webmin API is available, developed by Peter strand. Currently this is still under development.
A PHP replacement for web-lib.pl is also available, developed by Nicolas Faurant. I'm not sure what the devlopment status of this API is at the moment.