Developer Documentation

Virtualmin

Command Line API - Command line tools for managing Virtualmin.

Remote API - Remote access to the Virtualmin API.

Pre and Post Domain Modification Scripts - Writing your own scripts to be run when a virtual server is changed.

Writing Virtualmin Plugins - Building Webmin modules that interact with Virtualmin.

Creating Install Scripts - Making applications easily installable with the Virtualmin Install Scripts interface.

Creating New Content Styles - Creating new content styles for the Virtualmin website editor, or converting existing templates for use in Virtualmin.

Cloudmin

Command Line API - Command line tools for managing Cloudmin.

Remote API - Remote access to the Cloudmin API.

Contributing to Virtualmin or Cloudmin

Contributing -- Contributing documentation and patches.

Command Line API

Explains how to use the command-line scripts included with Virtualmin to manage users, aliases, servers, databases and resellers.

Introduction

Virtualmin comes a script named virtualmin that can be run from the Unix shell to perform actions that are usually done from the web interface. In fact, almost all actions that can be done in a browser can also be done from the command line, or from shell scripts. This allows virtual server, user and alias creation and management to be done in a more automated function, such as from programs or scripts of your own creation.

The first parameter to the virtualmin command is the operation you want to perform, such as create-domain. Depending on the operation, additional parameters may be needed, such as the name of the domain to create, password and so on. In almost all cases the parameters are given like --domain foo.com.

The virtualmin command must be run as root, as it needs access to system configuration files to create users, set up web sites and so on. If you want to create servers, users and aliases programatically as a different user (such as from your own CGI scripts), see the documentation on the Virtualmin Remote API instead.

All of the operations that make changes to the system output messages indicating their success or failure. Their exit status can also be checked to determine success - an exit status of 0 indicates that everything went OK, while a non-zero status indicates some problem.

All operations can be called with the --help command-line parameter to have them output details of the required and allowed parameters. Alternately, you can run virtualmin help to get a list of all available commands, or virtualmin help command to get information on what a particular command does.

Command Categories

API commands are broken down into the following categories :

Creating Install Scripts

Script Installers

A Virtualmin script installer is a small program that contains the information needed to install a web application into a virtual server's home directory, and configure it to run with that server's permissions and using its database. Most script installers are for PHP programs like phpMyAdmin, Drupal and SugarCRM, but it is possible to write an installer for Perl, Python or Ruby or Rails applications too.

Virtualmin Pro ships with a large number of built-in installers, which domain owners can add to their websites using the Install Scripts link on the left menu. However, there are many applications that are not covered yet, simply because we don't have time to implement installers for them or they are judged too rarely used or too specific. For this reason, Virtualmin provides an API for adding your own script installers.

Script Installer Files and Directories

Each script installer is a single file containing a set of Perl functions. Those that ship with Virtualmin Pro can be found in the virtual-server/scripts directory under the Webmin root, which is usually /usr/libexec/webmin or /usr/share/webmin . If you open up one of those files (such as phpbb.pl) in a text editor, you will see a series of funtions like :

sub script_phpbb_desc
{
return "phpBB";
}

sub script_phpbb_uses
{
return ( "php" );
}

sub script_phpbb_longdesc
{
return "A high powered, fully scalable, and highly customizable Open Source bulletin board package.";
}

Your own script installers will be in files if a similar format - the major difference will be the script ID, which appears in each function name after the word script_ .

Script installers that are local to your Virtualmin installation are stored in the /etc/webmin/virtual-server/scripts directory. In most cases, each script is just a single .pl file, but it is possible for other source or support files to be part of the script too. In general though, most script installers download the files they need from the website of the application that they are installing.

Script Installer IDs

Every script installer has a unique ID, which must consist of only letters, numbers and the underscore character. The ID determines both the installer filename (which must be scriptid.pl), and the names of functions within the script (which must be like script_scriptid_desc).

The same ID cannot be used by two different installers on the same system, even if one is built-in to Virtualmin and one is custom. For this reason, when writing an installer you should select an ID that is unlikely to clash with any that might be included in Virtualmin in the future. Starting it with the first part of your company's domain name (like foocorp_billingapp) would be a good way to ensure this.

The Lifetime of a Script

Virtualmin allows multiple instances of a single script to be installed, either on different domains or in different directories of the same domain. The installer defines the steps that must be taken to setup a script in some directory - in object-oriented coding parlance, it is like a class, while installed scripts are objects.

When a script is installed via the web interface, Virtualmin performs the following steps :

  1. Checks if all required dependencies are satisfied, such as required commands, a database and a website.
  2. If the script uses PHP, checks that the versions it supports are available on the system.
  3. Displays a form asking for installation options, such as the destination directory and database.
  4. Parses inputs from the form.
  5. Checks if the same script is already installed in the selected directory.
  6. Configures the domain's website to use the correct PHP version.
  7. Downloads files needed by the script, such as its source code.
  8. Installs any needed PHP modules, Pear modules, Perl modules or Ruby Gems.
  9. Calls the script's install function. This typically does the following :
    1. Creates a database for the script, if needed and if requested.
    2. Creates the destination directory.
    3. Extracts the downloaded source into a temporary directory.
    4. Copies the source to the destination directory.
    5. Updates any config files used by the application being installed, so that it knows how to connect to the database and which directory it runs in.
  10. Records the fact that the script has been installed.
  11. Configures PHP for the domain, to set any options that the script has requested.
  12. Restarts Apache.

Script Installer Implementation

In this section, the functions that each script installer must implement will be covered. Not all functions are mandatory - some deal with PHP dependencies that make no sense if your script does not use PHP, or if it has no non-core module or Pear dependencies. The example code for each function is taken from the Wordpress Blog installer, in wordpress.pl. This is a PHP application whose installation process is relatively simple, yet common to many other PHP programs.

In your own script, you would of course replace scriptname with the script ID you have selected.

Also, just like a Perl module -- make sure your Install Script file ends with the line:

1;

script_scriptname_desc

This function must return a short name for the script, usually a couple of words at most.

sub script_wordpress_desc
{
return "WordPress";
}

script_scriptname_uses

This must return a list of the languages the script uses. Supported language codes are php, perl and ruby. Most scripts will return only one.

sub script_wordpress_uses
{
return ( "php" );
}

script_scriptname_versions

Must return a list of versions of the script that the installer supports. Most can only install one, but in some cases you may want to offer the user the ability to install development and stable versions of some application. The version the user chooses will be passed to many other functions as a parameter.

sub script_wordpress_versions
{
return ( "2.2.1" );
}

script_scriptname_category (optional)

This function should return a category for the script, which controls how it is categorized in Virtualmin's list of those available to install. At the time of writing, available categories were Blog, Calendar, Commerce, Community, Content Management System, Database, Development, Email, Guestbook, Helpdesk, Horde, Photos, Project Management, Survey, Tracker and Wiki.

sub script_wordpress_category
{
return "Blog";
}

script_scriptname_php_vers (PHP scripts only)

Scripts that use PHP must implement this function, which should return a list of versions the installed application can run under. At the time of writing, Virtualmin only supports PHP versions 4 and 5. On systems that have more than one version of PHP installed, Virtualmin will configure the website to use the correct version for the path the script is installed to.

sub script_wordpress_php_vers
{
return ( 4, 5 );
}

script_scriptname_php_modules (PHP scripts only)

If the application being installed is written in PHP and requires any non-core PHP modules, this function should return them as a list. Any script that talks to a MySQL database will need the mysql module, or pgsql if it uses PostgreSQL. Virtualmin will attempt to install the required modules if they are missing from the system.

sub script_wordpress_php_modules
{
return ("mysql");
}

script_scriptname_pear_modules (PHP scripts only)

Pear is a repository of additional modules for PHP, which some Virtualmin scripts make use of. If the application you are installing requires some Pear modules, this function can be implemented to return a list of module names. At installation time, Virtualmin will check for and try to automatically install the needed modules.

sub script_horde_pear_modules
{
return ("Log", "Mail", "Mail_Mime", "DB");
}

script_scriptname_perl_modules (Perl scripts only)

For scripts written in Perl that require modules that are not part of the standard Perl distribution, you should implement this function to return a list of additional modules required. Virtualmin will try to automatically install them from YUM, APT or CPAN where possible, and will prevent the script from being installed if they are missing.

sub script_twiki_perl_modules
{
return ( "CGI::Session", "Net::SMTP" );
}

script_scriptname_python_modules (Python scripts only)

For scripts written in Python that require modules that are not part of the standard distribution, you should implement this function to return a list of additional modules required. Virtualmin will try to automatically install them from YUM or APT where possible, and will prevent the script from being installed if they are missing.

sub script_django_python_modules
{
return ( "setuptools", "MySQLdb" );
}

script_scriptname_depends(&domain, version)

This function must check for any dependencies the script has before it can be installed, such as a MySQL database or virtual server features. It is given two parameters - the domain hash containing details of the virtual server being installed into, and the version number selected.

sub script_wordpress_depends
{
local ($d, $ver) = @_;
&has_domain_databases($d, [ "mysql" ]) ||
        return "WordPress requires a MySQL database" if (!@dbs);
&require_mysql();
if (&mysql::get_mysql_version() < 4) {
        return "WordPress requires MySQL version 4 or higher";
        }
return undef;
}

As of Virtualmin 3.57, this function can return a list of missing dependency error messages instead of a single string, which is more user-friendly as they are all reported to users at once.

script_scriptname_dbs(&domain, version)

If defined, this function should return a list of database types that the script can use. At least one of these types must be enabled in the virtual server the script is being installed into.

sub script_wordpress_dbs
{
local ($d, $ver) = @_;
return ("mysql");
}

Only Virtualmin 3.57 and above make use of this function.

script_scriptname_params(&domain, version, &upgrade)

This function is responsible for generating the installation form inputs, such as the destination directory and target database. When upgrading (indicated by the upgrade hash being non-null) these are fixed and should just be displayed to the user. Otherwise, it must return inputs for selecting them. The functions return value must be HTML for form fields, generated using the ui_table_row and other ui_ functions.

The example below from Wordpress is a good source to copy from, as most PHP scripts that you would want to install will need a target directory and a database. The ui_database_select function can be used to generate a menu of databases in the domain, with an option to have a new one created automatically just for this script.

sub script_wordpress_params
{
local ($d, $ver, $upgrade) = @_;
local $rv;
local $hdir = &public_html_dir($d, 1);
if ($upgrade) {
        # Options are fixed when upgrading
        local ($dbtype, $dbname) = split(/_/, $upgrade->{'opts'}->{'db'}, 2);
        $rv .= &ui_table_row("Database for WordPress tables", $dbname);
        local $dir = $upgrade->{'opts'}->{'dir'};
        $dir =~ s/^$d->{'home'}\///;
        $rv .= &ui_table_row("Install directory", $dir);
        }
else {
        # Show editable install options
        local @dbs = &domain_databases($d, [ "mysql" ]);
        $rv .= &ui_table_row("Database for WordPress tables",
                     &ui_database_select("db", undef, \@dbs, $d, "wordpress"));
        $rv .= &ui_table_row("Install sub-directory under <tt>$hdir</tt>",
                             &ui_opt_textbox("dir", "wordpress", 30,
                                             "At top level"));
        }
return $rv;
}

script_scriptname_parse(&domain, version, &in, &upgrade)

This function takes the inputs from the form generated by script_scriptname_params, parses them an returns an object containing options that will be used when the installation actually happens. If it detects any errors in the input, it should return an error message string instead.

As in the example below, when upgrading the options are almost never changed, so it should return just $upgrade→{'opts'}, which are the options it was originally installed with. Otherwise, it should look at the hash reference in which will contain all CGI form variables, and use that to construct a hash of options. The most important keys in the hash are dir (the installation target directory) and path (the URL path under the domain's root).

sub script_wordpress_parse
{
local ($d, $ver, $in, $upgrade) = @_;
if ($upgrade) {
        # Options are always the same
        return $upgrade->{'opts'};
        }
else {
        local $hdir = &public_html_dir($d, 0);
        $in{'dir_def'} || $in{'dir'} =~ /\S/ && $in{'dir'} !~ /\.\./ ||
                return "Missing or invalid installation directory";
        local $dir = $in{'dir_def'} ? $hdir : "$hdir/$in{'dir'}";
        local ($newdb) = ($in->{'db'} =~ s/^\*//);
        return { 'db' => $in->{'db'},
                 'newdb' => $newdb,
                 'multi' => $in->{'multi'},
                 'dir' => $dir,
                 'path' => $in{'dir_def'} ? "/" : "/$in{'dir'}", };
        }
}

script_scriptname_check(&domain, version, &opts, &upgrade)

This function must verify the installation options in the opts hash, and return an error message if any are invalid (or undef if they all look OK). Possible problems include a missing or invalid install directory, a clash with an existing install of the same script in the directory, or a clash of tables in the selected database. As the example below shows, the find_database_table function provides a convenient way to search for tables by name or regular expression - for most applications, all tables used will be prefixed by a short code, like wp_ in the case of WordPress.

If you are wondering why these checks are not performed in script_scriptname_parse, the reason is that when a script is installed from the command line, that function is never called. Instead, install options are generated using a different method, and then validated by this function.

sub script_wordpress_check
{
local ($d, $ver, $opts, $upgrade) = @_;
$opts->{'dir'} =~ /^\// || return "Missing or invalid install directory";
$opts->{'db'} || return "Missing database";
if (-r "$opts->{'dir'}/wp-login.php") {
        return "WordPress appears to be already installed in the selected directory";
        }
local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
local $clash = &find_database_table($dbtype, $dbname, "wp_.*");
$clash && return "WordPress appears to be already using the selected database (table $clash)";
return undef;
}

script_scriptname_files(&domain, version, &opts, &upgrade)

This is the function where the script installer indicates to Virtualmin what files need to be downloaded for the installation to go ahead. Most scripts need only one, which contains the source code - but it is possible to request any number, even zero.

The function must return a list of hash references, each of which should contain the following keys :

  • name A unique name for this file, used later by the script_scriptname_install function.
  • file A short filename for the file, to which it will be saved in /tmp/.webmin after being downloaded.
  • url The URL that it can be downloaded from.
  • nocache Optional, but can be to 1 to force a download even if the URL is cached by Virtualmin.

In most cases, the ver parameter is used in the URL and filename to get the correct source archive. WordPress (shown below) is an exception, as it has only a single download URL which always serves up the latest version.


sub script_wordpress_files
{
local ($d, $ver, $opts, $upgrade) = @_;
local @files = ( { 'name' => "source",
           'file' => "latest.tar.gz",
           'url' => "http://wordpress.org/latest.zip",
           'nocache' => 1 } );
return @files;
}

script_scriptname_commands

If your script installer requires any commands to do its job that may not be available on a typical Unix system, this function should return a list of them. In most cases, it just returns the programs needed to un-compress the tar.gz or zip file containing the source.

sub script_wordpress_commands
{
return ("unzip");
}

script_scriptname_install(&domain, version, &opts, &files, &upgrade, username, password)

This function is where the real work of installing a script actually happens. It is responsible for setting up the database, un-compressing the downloaded source, copying it to the correct directory, modifying configuration files to match the domain and database, and returning a URL that can be used to login. If anything goes wrong, it must return an array whose first element is zero, and the second is an error message.

Upon success, it must return an an array containing the following elements :

  • The number 1 (indicating success)
  • An HTML message to display to the user. This should include a link that can be used to access the script.
  • A description of where it was installed, usually formatted like Under /wordpress using mysql database yourdomain.
  • The URL that can be used to access the script.
  • The initial administration login, if any.
  • The initial administration password.

If given, the username and password parameters should be used to set the initial administrative login for the script. If not, it should default to the domain's login and password.

The code snippets below show each step of the install process, taken from the standard WordPress installer. The first part simply parses the database connection options and creates a new DB for the script, if one was requested :

sub script_wordpress_install
{
local ($d, $version, $opts, $files, $upgrade) = @_;
local ($out, $ex);
if ($opts->{'newdb'} && !$upgrade) {
        local $err = &create_script_database($d, $opts->{'db'});
        return (0, "Database creation failed : $err") if ($err);
}
local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
local $dbuser = $dbtype eq "mysql" ? &mysql_user($d) : &postgres_user($d);
local $dbpass = $dbtype eq "mysql" ? &mysql_pass($d) : &postgres_pass($d, 1);
local $dbphptype = $dbtype eq "mysql" ? "mysql" : "psql";
local $dbhost = &get_database_host($dbtype);
local $dberr = &check_script_db_connection($dbtype, $dbname, $dbuser, $dbpass);
return (0, "Database connection failed : $dberr") if ($dberr);
}

The next step is to extract the downloaded source code, and then copy it to the created destination directory. This is done by calling the unzip and cp commands as the Virtualmin domain owner, so that there is no risk of files that he is not supposed to have access to being over-written. The source code temporary file can be found from the files hash reference in the source key, which was defined by the script_scriptname_files function.

Note how the code checks for expected files after extracting and copying the source, to make sure that the commands called actually succeeded.

# Create target dir
if (!-d $opts->{'dir'}) {
        $out = &run_as_domain_user($d, "mkdir -p ".quotemeta($opts->{'dir'}));
        -d $opts->{'dir'} ||
                return (0, "Failed to create directory : <tt>$out</tt>.");
        }

# Extract tar file to temp dir
local $temp = &transname();
mkdir($temp, 0755);
chown($d->{'uid'}, $d->{'gid'}, $temp);
$out = &run_as_domain_user($d, "cd ".quotemeta($temp).
                               " && unzip $files->{'source'}");
local $verdir = "wordpress";
-r "$temp/$verdir/wp-login.php" ||
        return (0, "Failed to extract source : <tt>$out</tt>.");

# Move html dir to target
$out = &run_as_domain_user($d, "cp -rp ".quotemeta($temp)."/$verdir/* ".
                               quotemeta($opts->{'dir'}));
local $cfileorig = "$opts->{'dir'}/wp-config-sample.php";
local $cfile = "$opts->{'dir'}/wp-config.php";
-r $cfileorig || return (0, "Failed to copy source : <tt>$out</tt>.");

Most scripts or applications have a configuration file of some kind that defines where to access the database, what domain they are running under, the URL path, and possibly an initial login and password. The script installers is responsible for creating or modifying this file to use the database connection details supplied by the opts paramater, as shown in the code snippet below.

Be careful when upgrading, as in general the existing configuration file will be valid for the new version. This means that it doesn't need to be re-created, and should be preserved during the upgrade process if necessary.

# Copy and update the config file
if (!-r $cfile) {
        &run_as_domain_user($d, "cp ".quotemeta($cfileorig)." ".
                                      quotemeta($cfile));
        local $lref = &read_file_lines($cfile);
        local $l;
        foreach $l (@$lref) {
                if ($l =~ /^define\('DB_NAME',/) {
                        $l = "define('DB_NAME', '$dbname');";
                        }
                if ($l =~ /^define\('DB_USER',/) {
                        $l = "define('DB_USER', '$dbuser');";
                        }
                if ($l =~ /^define\('DB_HOST',/) {
                        $l = "define('DB_HOST', '$dbhost');";
                        }
                if ($l =~ /^define\('DB_PASSWORD',/) {
                        $l = "define('DB_PASSWORD', '$dbpass');";
                        }
                if ($opts->{'multi'}) {
                        if ($l =~ /^define\('VHOST',/) {
                                $l = "define('VHOST', '');";
                                }
                        if ($l =~ /^\$base\s*=/) {
                                $l = "\$base = '$opts->{'path'}/';";
                                }
                        }
                }
        &flush_file_lines($cfile);
        }

In some cases, a script will come with a file of SQL statements that can be used to create and populate tables in its database. Others like WordPress do this automatically when they are first accessed. If the application you are installing needs SQL to be run as part of its setup process, you can use code like the fragment below, which was taken from the WebCalendar installer :

if (!$upgrade) {
        # Run the SQL setup script
        if ($dbtype eq "mysql") {
                local $sqlfile = "$opts->{'dir'}/tables-mysql.sql";
                &require_mysql();
                ($ex, $out) = &mysql::execute_sql_file($dbname, $sqlfile, $dbuser, $dbpass);
                $ex && return (0, "Failed to run database setup script : <tt>$out</tt>.");
                }

The final part of the script_scriptname_install function is returning information to Virtualmin about how to access the new script, and where it is installed. In some cases, a script will have two URLs - the one for administration (which should be references in the second element of the returned array), and the one for general use (which should be in the 4th element).

local $url = &script_path_url($d, $opts).
             ($upgrade ? "wp-admin/upgrade.php" : "wp-admin/install.php");
local $userurl = &script_path_url($d, $opts);
local $rp = $opts->{'dir'};
$rp =~ s/^$d->{'home'}\///;
return (1, "WordPress installation complete. It can be accessed at <a href='$url'>$url</a>.", "Under $rp using $dbphptype database $dbname", $userurl);
}

script_scriptname_uninstall(&domain, version, &opts)

This function is responsible for cleaning up all files and database tables created by the install code. It is only called when the user deletes a script from a domain, not when upgrading. If most cases, determining which tables to remove is simple, as they all start with some prefix (like wp_ in the case of WordPress). But if a script has created a tables that cannot be automatically identified, your installer will need to have this list hard-coded. See the oscommerce.pl installer for an example.

If the installer has created any cron jobs, server processes, custom Apache configuration entries or email aliases, they must also be removed by this function. On success, it should return a two-element array whose first element is 1, and the second a message to display to the user. On failure, it should return 0 and an error message explaining what went wrong.

sub script_wordpress_uninstall
{
local ($d, $version, $opts) = @_;

# Remove the contents of the target directory
local $derr = &delete_script_install_directory($d, $opts);
return (0, $derr) if ($derr);

# Remove all wp_ tables from the database
local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
if ($dbtype eq "mysql") {
        # Delete from MySQL
        &require_mysql();
        foreach $t (&mysql::list_tables($dbname)) {
                if ($t =~ /^wp_/) {
                        &mysql::execute_sql_logged($dbname,
                                "drop table ".&mysql::quotestr($t));
                        }
                }
        }
else {
        # Delete from PostgreSQL
        &require_postgres();
        foreach $t (&postgresql::list_tables($dbname)) {
                if ($t =~ /^wp_/) {
                        &postgresql::execute_sql_logged($dbname,
                                "drop table $t");
                        }
                }
        }

# Take out the DB
if ($opts->{'newdb'}) {
        &delete_script_database($d, $opts->{'db'});
        }

return (1, "WordPress directory and tables deleted.");
}

script_scriptname_passmode(&domain, version)

Most scripts that setup an initial login and password use those from the virtual server the script is being added to. However, Virtualmin can prompt the user for alternative authentication details if you implement this function. All it has to do is return one of the following numeric codes :

  • 1 - Script needs a username and password
  • 2 - Script only needs a password
  • 3 - Script only needs a username

The custom login and password entered by the user will be passed to the script_scriptname_install function. If your script installer doesn't setup an initial login at all, you can either omit this function or have it return 0.

sub script_wordpress_passmode
{
return 1;
}

Other Installation Methods

The style of installation code above will work for most scripts that you want to install, but in some cases a slightly different approach is needed. This section covers two of them - scripts with their own configuration generators that cannot be easily replaced by creating the config file yourself, and installers for Ruby On Rails apps, which use a separate server process.

HTTP Requests

Many PHP applications come with a script that asks the user a series of questions, like the database login and name, domain name, and initial administration username and password. The script then uses this information to create a config file and perhaps populated the database.

Ideally, Virtualmin script installers should create any needed config files directly - but in some cases this is too difficult due to their complexity. Similarly, it may not be possible to create and populate all the needed database tables if no SQL file is provided for doing this. In cases like this, it is simpler for a script installer to invoke the application's install code directly, by making an HTTP request to the correct URL.

To figure out the installation URL and the CGI parameters it needs, you will need to install the application manually and run through its install process in a browser. The View source feature can then be used to find the names and meanings of all form fields, which can then be used to construct code to call the script the form would submit to.

The following code fragment from the script_phpbb_install function of the phpbb.pl installer gives an example of this :

# Make config.php writable
&make_file_php_writable($d, $cfile);

# Trigger the installation PHP script
local @params = (
        [ "lang", "english" ],
        [ "dbms", $dbtype eq "mysql" ? "mysql4" : "postgres" ],
        [ "upgrade", 0 ],
        [ "dbhost", $dbhost ],
        [ "dbname", $dbname ],
        [ "dbuser", $dbuser ],
        [ "dbpasswd", $dbpass ],
        [ "prefix", "phpbb_" ],
        [ "board_email", $d->{'emailto'} ],
        [ "server_name", "www.".$d->{'dom'} ],
        [ "server_port", $d->{'web_port'} ],
        [ "script_path", $opts->{'path'}."/" ],
        [ "admin_name", $d->{'user'} ],
        [ "admin_pass1", $d->{'pass'} ],
        [ "admin_pass2", $d->{'pass'} ],
        [ "install_step", 1 ],
        [ "current_lang", "english" ],
        );
local $params = join("&", map { $_->[0]."=".&urlize($_->[1]) } @params);
local $ipage = $opts->{'path'}."/install/install.php";

# Make an HTTP post to the installer page
local ($iout, $ierror);

&post_http_connection("www.$d->{'dom'}", $d->{'web_port'},
                      $ipage, $params, \$iout, \$ierror);
if ($ierror) {
        return (0, "phpBB post-install configuration failed : $ierror");
        }
elsif ($iout !~ /Finish Installation/i) {
        return (0, "phpBB post-install configuration failed");
        }

As you can see, it makes use of the post_http_connection function provided by Virtualmin which makes an HTTP POST request, which is expected by most applications. If the form is submitted using a GET, you could use Webmin's http_download function instead.

In some cases, the installation process is a multi-step wizard, which means that you will need to make several POST requests with different parameters, and possibly parse the output from each. In the worst case, the application may set cookies to track the progress of the wizard - see the SugarCRM installer in sugarcrm.pl for an example of this.

Ruby On Rails Installation

Most Rails applications installed by Virtualmin are actually run by a separate server process, typically a Mongrel webserver. To link them up to the domain's actual website, Apache proxy directives are added that pass all requests to a path like /typo to a local webserver at a URL like http://localhost:3001/typo. Starting and maintaining this server process and configuring Apache to use it requires a fair bit of work, but fortunately Virtualmin Pro version 3.44 and above include functions that make it easier.

Some Ruby applications are available from the Ruby Gems package installation service, while others must be downloaded and extracted like PHP applications. For example, typo.pl does installation entirely from a Gem, and so has a script_typo_files function that returns nothing. It then makes use of the install_ruby_gem function to Install gems for Mongrel and Typo itself.

The code fragment below is the first part of the script_typo_install function. As you can see, it checks for the gem command, and then calls functions to install those Gems that it needs.

sub script_typo_install
{
local ($d, $version, $opts, $files, $upgrade) = @_;
local ($out, $ex);

# Check for the gem command (here instead of earlier, as it may have been
# automatically installed).
&has_command("gem") || return (0, "The Ruby gem command is not installed");

# Create target dir
if (!-d $opts->{'dir'}) {
        $out = &run_as_domain_user($d, "mkdir -p ".quotemeta($opts->{'dir'}));
        -d $opts->{'dir'} ||
                return (0, "Failed to create directory : <tt>$out</tt>.");
        }

# Install mongrel first
local $err = &install_ruby_gem("mongrel");
if ($err) {
        return (0, "Mongrel GEM install failed : <pre>$err</pre>");
        }

# Install typo itself
local $err = &install_ruby_gem("typo");
if ($err) {
        return (0, "Typo GEM install failed : <pre>$err</pre>");
        }
if (!&has_command("typo")) {
        return (0, "Install appear to succeed, but the <tt>typo</tt> command ".
                   "could not be found");
        }

Just installing the Gem is not enough - the code for Typo needs to be somehow copied into the virtual server's directory. Fortunately, Typo provides a command to do this, shown in the code below. Other Ruby applications are distributed in tar.gz files, and need to be extracted and copied into place.

$out = &run_as_domain_user($d, "cd ".quotemeta($opts->{'dir'})." && ".
                               "typo install .");
if ($?) {
        return (0, "Typo setup failed : <pre>$out</pre>");
        }

Because Rails applications use a separate server process, your installer must find a free port for it to run on, and then start it. Virtualmin provides the allocate_mongrel_port function to do the former, and the mongrel_rails_start_cmd function to build a command for the latter. Some Rails applications (like Typo) provide their own server startup scripts, so check the documentation to see which commands they recommend.

$out = &run_as_domain_user($d, "cd ".quotemeta($opts->{'dir'})." && ".
                               "typo start .");
if ($?) {
        return (0, "Failed to start Typo server : <pre>$out</pre>");
        }

All Rails applications will need an Apache proxy configuration to direct requests from the Apache webserver to their Mongrel server. Virtualmin provides a simple function to set this up, shown in the code below. Be aware that many Rails apps don't like being run in a sub-directory, and most don't automatically support it. Your installer may need to modify configuration files and perhaps even application code to support this.

&setup_mongrel_proxy($d, $opts->{'path'}, $opts->{'port'},
                     $opts->{'path'} eq '/' ? undef : $opts->{'path'});

The server process that runs the Rails application must be running all the time - but what happens if the system gets rebooted? To handle this, Virtualmin makes it easy to create either an @reboot crontab entry or /etc/init.d script to start the server, as shown in the following code. The init script will only be added if the domain has the new Bootup Actions plugin installed and made available.

if (!$upgrade) {
        # Configure server to start at boot
        local $typo = &has_command("typo");
        local $startcmd = "cd ".quotemeta($opts->{'dir'})."; ".
                          "$typo start . 2>&1 </dev/null";
        local $stopcmd = "kill `cat ".quotemeta($pidfile)."`";
        &setup_mongrel_startup($d, $startcmd, $stopcmd, $opts,
                               1, "typo-".$opts->{'port'}, "Typo Blog Engine");
        }

Ruby On Rails Un-Installation

Just as special code is required to install Rails applications, your script must also implement the script_scriptname_uninstall function to clean up all server processes, boot scripts and Apache config entries. This is made easier by several convenience functions provided by Virtualmin, the use of which is show in the code below:

sub script_typo_uninstall
{
local ($d, $version, $opts) = @_;

# Shut down the server process
local $pidfile = "$opts->{'dir'}/tmp/pid.txt";
local $pid = &check_pid_file($pidfile);
if ($pid) {
        kill('KILL', $pid);
        }

# Remove bootup script

&delete_mongrel_startup($d, $opts, "typo start .");

# Remove the contents of the target directory
local $derr = &delete_script_install_directory($d, $opts);
return (0, $derr) if ($derr);

# Remove proxy Apache config entry for /typo
&delete_mongrel_proxy($d, $opts->{'path'});
&register_post_action(\&restart_apache);

return (1, "Typo directory deleted.");
}

When Virtualmin deletes a domain, it does not call the uninstall functions for any installed scripts, as this would generally be a waste of time - their directories and databases are going to be removed anyway. In the case of Rails applications, this is not true - their installers start server processes and create boot scripts that must be cleaned up.

To ensure that this happens, your script installer must implement the script_scriptname_stop function, which only has to shut down any server processes and prevent them from being run at boot time. This function is only called at virtual server deletion time, and is optional for installers that don't require it.

sub script_typo_stop
{
local ($d, $sinfo) = @_;
local $pidfile = "$sinfo->{'opts'}->{'dir'}/tmp/pid.txt";
local $pid = &check_pid_file($pidfile);
if ($pid) {
        kill('KILL', $pid);
        }
&delete_mongrel_startup($d, $sinfo->{'opts'}, "typo start .");
}

Creating New Content Styles

Creating New Content Styles

Virtualmin Professional includes a fast website creator that allows users to build websites from templates with just a couple of clicks. Once built, the end user can edit the resulting website using a built-in WYSIWYG editor (or using other tools).

Virtualmin includes a few styles that are royalty free and usable for any purpose, commercial or otherwise. Adding new styles is easy, and we encourage web designers to create their own templates for sale - making an existing template into a Virtualmin Content Style is a very simple process.

Directory Structure

The content styles included with Virtualmin are located in the virtual-server/styles directory under the Webmin root. This may be /usr/libexec/webmin/virtual-server/styles (RHEL/CentOS/Fedora/SuSE/Mandriva) or /usr/share/webmin/virtual-server (Debian/Ubuntu). Styles you create yourself should go in /etc/webmin/virtual-server/styles - you may need to create that directory if it doesn't exist yet. Each style should have its own directory, named similarly to the name of the style. Style names should be unique. Including a STYLE-LICENCE.txt file is optional, but recommended if your style is not freely re-distributable.

Here is an example directory and file tree for a simple but complete style with three pages (index.html, sales.html, contact.html):

newstyle/ newstyle/css newstyle/css/style.css newstyle/index.html newstyle/sales.html newstyle/STYLE-LICENCE.txt newstyle/template.html newstyle/thumb.png newstyle/images newstyle/images/footer.jpg newstyle/images/header.jpg newstyle/contact.html newstyle/style.info

Create the sub-directories under styles, each of which contains the template HTML and image files for your own style. The only other special file that needs to go in a style directory is style.info, which should contain lines like:

desc=My Custom Theme name=newstyle

The desc= line specifies the description of the style as it appears in Virtualmin, while the name= line defines the directory it will be stored in under /etc/webmin/virtual-server/themes when installed on another system. No two styles should have the same name.

The other bit you'll want to create is a thumbnail image of the style. The format of this is PNG at 500x375 pixels. Call this file thumb.png. If you will be contributing your style for inclusion in Virtualmin Professional, you'll also want to include an HTML file called template.html which is a version of your index.html with suitable example content included instead of the variables mentioned below.

Within the .html files you can use variables ${CONTENT}, ${OWNER}, ${DOM} to tell Virtualmin where to put the user data (which can all be edited later in the WYSIWYG editor).

Packaging

Just like all other types of packages for Virtualmin, Webmin and Usermin, Content Styles can be bundled into tarballs, optionally gzipped. They can also be zipped, if you're more comfortable with zip format files.

To create a Content Style package, at the directory styles within the Virtualmin virtual-server directory, execute a command like the following:

cd /etc/webmin/virtual-server/styles/newstyle tar czvf /tmp/newstyle.tar.gz .

This can then be easily installed using the System SettingsContent Styles page in Virtualmin. If you have the rights necessary to distribute the work (e.g. if you created the style, or if it is based on a Creative Commons licensed redistributable template), feel free to post a link in the forums so others can enjoy it. We also encourage commercial template developers to get involved. If you've already got a stable of templates, converting them for us in Virtualmin is simple, and could provide an additional source of revenue for your existing work. Let us know if you've built commercial Virtualmin Content Styles, and we'll provide a free link to your website.

Pre and Post Domain Modification Scripts

Introduction to Modification Scripts

Virtualmin provides a method for you to have your own scripts (typically shell scripts, but Perl and Python also work) run before or after a virtual server is created, modified or deleted.

This can be useful if you want to have some setup performed for each new domain that is beyond Virtualmin's built-in capabilities. They can also be used to validate changes made by a user before they happen, such as creation of a domain with settings that are inappropriate for your system.

Virtualmin lets you specify a script to be run before any action is performed, and a separate script to be run after. If the pre-change script fails (by returning a non-zero exit status), whatever action was going to happen will be blocked.

Example Scripts

Let's imagine you wanted each new virtual server to have a file it is home directory downloaded from some website. A post-creation script to implement this would look like :

#!/bin/sh
if [ "$VIRTUALSERVER_ACTION" = "CREATE_DOMAIN" ]; then
  cd $VIRTUALSERVER_HOME
  wget -O somefile.tar.gz "http://yourdomain.com/somefile.tar.gz"
  chown $VIRTUALSERVER_USER:$VIRTUALSERVER_GROUP somefile.tar.gz
fi

If you had some external database that you wanted each new domain user to have an account in, you could automate this with a script like :

#!/bin/sh
if [ "$VIRTUALSERVER_PARENT" = "" ]; then
  if [ "$VIRTUALSERVER_ACTION" = "CREATE_DOMAIN" ]; then
    /usr/bin/add-to-database.pl $VIRTUALSERVER_USER $VIRTUALSERVER_PASS
  fi
  if [ "$VIRTUALSERVER_ACTION" = "DELETE_DOMAIN" ]; then
    /usr/bin/remove-from-database.pl $VIRTUALSERVER_USER
  fi
fi

Setting Pre and Post Modification Scripts

Once you have written a script, you can tell Virtualmin which scripts to run before or after some action is performed as follows :

  1. Login as root and go to System Settings -> Virtualmin Configuration.
  2. Select the Actions upon server and user creation category.
  3. In the Command to run before making changes to a server field enter the full path to your pre-modification command, if any.
  4. In the Command to run after making changes to a server field, enter the full path to the post-modification command.
  5. Click Save.

Make sure the script(s) are executable, or else Virtualmin will not be able to run them.

Variables Available In Scripts

Every attribute of a virtual server is available via environment variables, all of which start with the $VIRTUALSERVER_ prefix. These are the same variables that can be used in email and other templates, as documented on the Template Variable Listing page. However, all script variables have $VIRTUALSERVER_ at the start, like $VIRTUALSERVER_DOM or $VIRTUALSERVER_PASS.

The action being performed can be determined by looking at the $VIRTUALSERVER_ACTION environment variable, which will be set to one of :

CREATE_DOMAIN A new virtual server is being created MODIFY_DOMAIN Some attribute of a virtual server is being changed DELETE_DOMAIN A virtual server has been deleted DISABLE_DOMAIN The virtual server is being temporarily disabled ENABLE_DOMAIN The virtual server is being re-enabled DBNAME_DOMAIN The database login for a virtual server is being changed DBPASS_DOMAIN The database password for a virtual server is being changed RESTORE_DOMAIN A virtual server is being restored from a backup

Remote API

Explains how to manage Virtualmin servers, users, databases and other features remotely from other programs.

Introduction

Even though a command-line API exists for managing Virtualmin objects such as servers, users and databases, this may not be appropriate or usable in all circumstances. Because the commands need to be run as root, they cannot be called from PHP or CGI scripts invoked by a web server, which typically run as a less-privileged Apache user. Also, they must be run on the server running Virtualmin, which makes them difficult to call from another system.

For this reason, an alternate method exists for running these programs via HTTP requests. A special URL in the Virtualmin web interface exists to be called by other programs, and to then pass its parameters on to one of the command-line scripts. This URL can be requested from any system, and by processes running as any Unix user.

Before reading this documentation, you should be familiar with the Virtualmin Command Line Programs documentation, even if you never use those commands directly.

The Remote API

All remote calls must be made through the CGI /virtual-server/remote.cgi . The full URL for this will be https://yourserver:10000/virtual-server/remote.cgi , where 'yourserver' is the full hostname or IP address of the system running Virtualmin.

This URL must be provided with at least one parameter named 'program', whose value must be the name of the command-line program to invoke, without the .pl extension. So a possible URL to request would be :

https://yourserver:10000/virtual-server/remote.cgi?program=list-domains

Because most command-line programs require additional parameters, these must be included in the URL. Every CGI parameter is converted to a command-line parameter, with the value of the parameter appended if given. For example, to create a mail alias, you could invoke the URL :

https://yourserver:10000/virtual-server/remote.cgi?program=create-alias&domain=foo.com&from=sales&to=joe@foo.com

To specify a parameter that does not have anything after it, just add a CGI parameter with no value. For example, to list databases in detailed form, you could call :

https://yourserver:10000/virtual-server/remote.cgi?program=list-databases&domain=foo.com&multiline=

Both GET and POST format HTTP requests can be used. If your Virtualmin server is not running in SSL mode, use http:// instead of https:// in the URL.

Reading Output

The page returned by the remote.cgi URL will always be plain text, and will contain the output produced by the command-line program. One additional line will be appended though - the words 'Exit status:' followed by the numeric Unix exit status of the command. A status of 0 means that the program was successful, while a non-zero status indicates some error. The cause of the error can be determined from the command's output.

JSON and XML Output

If you would prefer JSON format output, add the json=1 parameter to the remote API call. Alternately, you can select XML format with the xml=1 parameter. Finally, Perl format can be selected with the perl=1 parameter. Make sure you also add the multiline= parameter to any list-* API calls, or else conversion to JSON or XML will not happen.

Authentication

Because the remote API is part of the Virtualmin web interface, it is password-protected using the same authentication system as you would normally use to login via a web browser. Only the master administrator can access the remote API though, as it can be used to perform almost any action in Virtualmin, and thus would be unsafe for server owners or resellers to use.

When calling the remote API from your own programs, the HTTP request must have the necessary HTTP authentication headers. All HTTP libraries and programs have options to set the username and password - for example, the wget command uses --http-user and --http-passwd , so you could fetch a list of domains from a remote Virtualmin server with a command like :

wget--http-user=root --http-passwd=smeg 'https://yourserver:10000/virtual-server/remote.cgi?program=list-domains'

Example Usage

PHP Example

<?php
$result = shell_exec("wget -O - --quiet --http-user=root --http-passwd=pass --no-check-certificate 'https://localhost:10000/virtual-server/remote.cgi?program=list-domains'");

echo $result;
?>

Perl Example

#!/usr/bin/perl -w
 
$result = `wget -O - --quiet --http-user=root --http-passwd=pass --no-check-certificate 'https://localhost:10000/virtual-server/remote.cgi?program=list-domains'`;
print "$result\n";
 

Perl LWP Example

#!/usr/bin/perl -w
 
use strict;
use LWP;
 
my $browser = LWP::UserAgent->new( );
$browser->credentials("localhost:10000", "Webmin Server", "root" => "pass");

 
my $result = $browser->get( "https://localhost:10000/virtual-server/remote.cgi?program=list-domains" );
 
print $result->content;
 

Writing Virtualmin Plugins

Writing Virtualmin Plugins

Introduction to Plugins

Before starting on a plugin, we suggest that you first read the Webmin module developers guide at http://doxfer.com/Webmin/ModuleDevelopment .

A Virtualmin plugin is simply a Webmin module that can provide additional features to Virtualmin virtual servers or users. To do this, it must contain a Perl script called virtual_feature.pl which defines certain functions. The plugin module can then be registered by Virtualmin, and the feature it offers will then be available when creating new virtual domains.

A plugin typically adds a new possible to virtual servers, in addition to the standard features built into Virtualmin (website, DNS domain and so on). For example, it may enable reporting using some new statistics generator, or activate some game server in the virtual domain. Virtualmin will add options to the Create Server and Edit Server pages for enabling the plugin's feature, and call functions in its virtual_feature.pl when the feature is activated, de-activated or changed.

Starting a Plugin

The steps to start writing your own plugin are similar to those for creating a new Webmin module :

  1. Find the Webmin root directory, which will be /usr/libexec/webmin on Redhat-derived systems, /usr/share/webmin on Debian and Ubuntu, or /opt/webmin on Solaris.
  2. Pick a directory name for your plugin that is not currently in use by any other Webmin module. Plugin directories typically start with virtualmin- - so for the purposes of this documentation, we will assume that yours is going to be called virtualmin-yourname .
  3. Create that directory under the Webmin root. Then within it, create help and lang sub-directories.
  4. Create a file in the directory called module.info. This should contain lines like :
desc=A description of your plugin
version=1.0
hidden=1

Naturally, the desc= line should be changed to something more meaningful. If you want the plugin to appear in Webmin's menu of modules, remove the hidden=1 line. This is not typically the case, as most plugins are accessed entirely via Virtualmin.

  1. Create a module library file for your plugin, named virtualmin-yourname-lib.pl. Initially, it can contain the following code :
use WebminCore;
&init_config();
&foreign_require("virtual-server", "virtual-server-lib.pl");
1;

  1. Create a plugin API file called virtual_feature.pl, containing the following initial code :
do 'virtualmin-yourname-lib.pl';

sub feature_name
{
return "A description of your plugin";
}
  1. Delete the file /etc/webmin/module.infos.cache to clear Webmin's cache of installed modules.
  2. Open Virtualmin in your browser, and click on Features and Plugins under System Settings on the left menu. Your new plugin should appear in the list of those available - check its box in the left column, and click Save.

With this done, you can now start work on expanding the capabilities of the plugin by implementing the API documented below.

Package and Distribution

Since a plugin is just a Webmin module, the usual process for packaging it still applies. The commands to do this are :

cd /usr/libexec/webmin
tar cvzf /tmp/virtualmin-yourname.wbm.gz virtualmin-yourname

As you can see, a module or plugin is just a tar file of the directory. These can then be installed using the Webmin Configuration module, on the Webmin Modules page.

If you prefer to package your plugin as an RPM, this can be done using the makemodulerpm.pl script, available from http://www.webmin.com/makemodulerpm.pl . It must be run as root as shown below :

cd /usr/libexec/webmin
/usr/local/bin/makemodulerpm.pl --target-dir /tmp virtualmin-yourname

This will create a file named wbm-virtualmin-yourname-1.0-1.noarch.rpm in the /tmp directory.

A similar script exists for Debian and Ubuntu systems, available from http://www.webmin.com/makemoduledeb.pl .

Plugin CGI Scripts

Because a plugin is just a Webmin module, it can contain .cgi scripts like any other module. These can be useful for displaying additional information about the feature that the plugin manages, or for managing objects within that feature (such as mailing lists or user accounts). Most plugins will not need to include any CGI scripts, as their functionality is provided entirely by implementing the API functions described below.

You can see some examples of this by looking at the Mailman and Oracle plugins. The former includes several CGIs for managing mailing lists in a domain, while the Oracle plugin has CGIs for creating and viewing tables within databases created by Virtualmin.

The Plugin API

The meat of a plugin is it's implementation of an API that is called by Virtualmin when performing tasks like enabling or disabling the plugin's feature for a domain. These must all be defined in the file virtual_feature.pl under the plugin's directory. Most are optional, depending on which functionality your plugin is implementing.

For each function the name, supplied parameters, description and an example implementation are shown.

Many functions are passed a domain object as a parameter. This is simply a hash reference that is used internally by Virtualmin to store information about a virtual server. Some of the useful keys in the hash are :

In addition, for each feature the domain has enabled, the code for that feature will be set to 1 in the hash. For example, a domain with a website has web set to 1, for email the code is mail, and for a Webmin login the code is webmin. Virtualmin will add an entry for your plugin when its feature is enabled for the domain, the code for which will be the same as the plugin's directory.

Core Functions

Functions in this section must be implemented by all plugins.

feature_name()

This must return a short name for the feature, like My plugin.

sub feature_name
{
return "My plugin name";
}

Functions for Features

The most common use for plugins is to add a new feature that can be selected when a virtual server is created or modified. The functions listed in this section should be implemented in this case, although not all are mandatory.

feature_label(edit-form)

This must return a longer name for the feature, for display in the server creation and editing pages. The edit-form parameter can be used to determine which page is calling the function.

sub feature_label
{
return "Enable my plugin";
}

feature_check()

This optional function will be called when the plugin is registered by Virtualmin, to check that all of its dependencies are met. It must return undef if everything is OK, or an error message if some program or service that the plugin depends upon is missing. If not implemented, Virtualmin will assume that the plugin has no dependencies.


sub feature_check
{
if (! &has_command("someprogram")) {
  return "The commmand someprogram required by this plugin is not installed";
  }
else {
  return undef;
  }
}

feature_losing(domain)

This should return text to be displayed when this feature is being removed from a domain. The domain parameter is the Virtualmin domain hash reference for the server the feature is being removed from.

sub feature_losing
{
return "My plugin for this virtual server will be disabled";
}

feature_disname(domain)

This optional function should return a description of what will be done when this feature is temporarily disabled. It is only needed if your plugin implements the feature_disable function, indicating that it can be disabled.

sub feature_disname
{
return "My plugin will be temporarily de-activated";
}

feature_clash(domain)

If activating this plugin feature in the given domain would clash with something already on the system, this function must return an error message. Otherwise, it can just return undef.

sub feature_clash
{
my ($d) = @_;
if (-r "/etc/someprogram/$d->{'dom'}") {
  return "Some program is already enabled for this domain";
  }
else {
  return undef;
  }
}

feature_depends(domain)

If implemented, this function should check if the given domain object has all the features enabled that would be required by this plugin. For example, if your plugin implements something that is accessible via the web, the domain must have the web feature set. If a dependency is missing it must return an error message explaining this, or undef if everything is OK.

sub feature_depends
{
my ($d) = @_;
if ($d->{'web'}) {
  return undef;
  }
else {
  return "My plugin requires a website";
  }
}

feature_suitable(parentdom, aliasdom, superdom)

This function should check the given parent domain, alias target domain and super-domain objects, to ensure that they are suitable for this feature. It can be useful for preventing the plugin from being enabled in sub-domains or alias domains, where it may not be appropriate. It must return 1 if the feature can be used, or 0 if not. If not implemented, Virtualmin will not allow use of your plugin.


sub feature_suitable
{
my ($parent, $alias, $super) = @_;
if ($parent && !$parent->{'web'}) {
  return 0;
  }
else {
  return 1;
  }
}

feature_setup(domain)

This function will be called when the plugin feature is being enabled for some server, either at creation time or when the server is subsequently modified. It must perform whatever actions are needed, such as modifying config files, running commands and so on. It should notify the user of the features activation by calling the functions &$virtual_server::first_print and &$virtual_server::second_print, like so :

sub feature_setup
{
my ($d) = @_;
&$virtual_server::first_print("Setting up My plugin..");
my $ex = system("somecommand --add $d->{'dom'} >/dev/null 2>&1");
if ($ex) {
  &$virtual_server::second_print(".. failed!");
  return 1;
  }
else {
  &$virtual_server::second_print(".. done");
  return 1;
  }
}

feature_delete(domain)

This function is called when the feature is removed for some server, either at deletion time or when the server is modified. It must perform whatever config file changes or run whatever commands are needed to turn the feature off, and should use the &$virtual_server::first_print and &$virtual_server::second_print functions to notify the user about what it is doing.

sub feature_delete
{
my ($d) = @_;
&$virtual_server::first_print("Turning off up My plugin..");
system("somecommand --remove $d->{'dom'} >/dev/null 2>&1");
&$virtual_server::second_print(".. done");
}

feature_modify(domain, olddomain)

Whenever a virtual server is modified, this function will be called in all plugins. It should check if some attribute of the server that the plugin uses has changed (like dom or user), and update the appropriate config files. For example, if your feature configures some program that needs to know the virtual server's domain name, this function must compare $domain→{'dom'} and $olddomain→{'dom'} , and if they differ perform whatever updates are needed. It should only produce output when it actually does something though.

sub feature_modify
{
my ($d, $oldd) = @_;
if ($d->{'dom'} ne $oldd->{'dom'}) {
  &$virtual_server::first_print("Changing domain for My plugin ..");
  rename("/etc/someprogram/$oldd->{'dom'}", "/etc/someprogram/$d->{'dom'}");
  &$virtual_server::second_print(".. done");
  }
}

feature_disable(domain)

If this function is defined, it will be called when a virtual server with the plugin feature active is disabled. It should temporarily turn off access to the feature in a non-destructive way, so that it can be fixed later by a call to feature_enable.

sub feature_disable
{
my ($d) = @_;
&$virtual_server::first_print("Temporariliy disabling My plugin ..");
rename("/etc/someprogram/$d->{'dom'}", "/etc/someprogram/$d->{'dom'}.disabled");

&$virtual_server::second_print(".. done");
}

feature_enable(domain)

This function will be called when a virtual server with the plugin's feature is re-enabled. It should undo whatever changes were made by the feature_disable function. It only needs to be implemented if feature_disable is.

sub feature_enable
{
my ($d) = @_;
&$virtual_server::first_print("Re-enabling My plugin ..");
rename("/etc/someprogram/$d->{'dom'}.disabled", "/etc/someprogram/$d->{'dom'}");
&$virtual_server::second_print(".. done");
}

feature_bandwidth(domain, start, bwhash)

If defined, this function should report to Virtualmin the amount of bandwidth used by some virtual server since the given start Unix time. Bandwidth is the total number of bytes uploaded and downloaded, broken down by day. This function should scan whatever log file is available for the feature, extract upload and download counts for the domain, and add to the values in the bwhash hash reference.

Because bandwidth is accumulated by day, the bwhash hash is index by the number of days since 1st Jan 1970 GMT, which is simply a Unix time divided by 86400.

feature_webmin(domain, other-domains)

If you want your plugin to provide access to a Webmin module to the owners of virtual servers that have its feature enabled, this function can be used tell Virtualmin which modules access should be granted to. Typically, a plugin will grant access to its own module, which will have standard CGI scripts for use in further configuring whatever service the plugin enables.

This function must return a list of array references, each of which has two values :

  1. The directory of a module to grant access to (typically just $module_name)
  2. A hash reference of ACL values to set in that module for the domain owner. This is typically used to restrict him to just the configurations relevant to the given domain.

The domain parameter is the virtual server object that this feature is enabled in, and other-domains is an array reference of other virtual servers that are owned by the same user as domain, and which have the plugin's feature enabled. This latter parameter should be taken into account in order to grant access to configure all of the user's servers.

sub feature_webmin
{
local ($d, $all) = @_;
my @fdoms = grep { $_->{$module_name} } @$all;
if (@fdoms) {
  return ( [ $module_name, { 'doms' => join(" ", @fdoms) } ] );
  }
else {
  return ( );
  }
}

feature_import(domain-name, user-name, db-name)

This function is called when an existing virtual server is being imported into Virtualmin. It should return 1 if the service configured by the plugin is already active for the given domain, perhaps because it was set up manually.

sub feature_import
{
my ($dname, $user, $db) = @_;
if (-r "/etc/someprogram/$dname") {
  return 1;
  }
else {
  return 0;
  }
}

feature_links(domain)

This optional function allows the plugin to provide additional links on the left menu of framed Virtualmin themes when a domain with the feature enabled is selected. It must return a list of hash references, each containing the following keys :

A link to a module that the current Webmin user does not have access to will not be displayed. This means that you should almost always define feature_webmin as well, and make sure it returns the plugin's module.

sub feature_links
{
local ($d) = @_;
return ( { 'mod' => $module_name,
           'desc' => "Manage My plugin",
           'page' => 'index.cgi?dom='.$d->{'dom'},
           'cat' => 'services',
         } );
}

feature_always_links(domain)

This function is similar to feature_links, but is called regardless of which domain is selected. It can be used when you have a page that can be used even for virtual servers that don't have the plugin's feature active.

feature_validate(domain)

This function is optional, and is used by Virtualmin domain validation page. If implemented, it should check to ensure that all configuration files and other settings specific to the domain are setup properly. If any problems are found it should return an error message string, otherwise undef.

sub feature_validate
{
my ($d) = @_;
if (!-r "/etc/someprogram/$d->{'dom'}") {
  return "Missing someprogram configuration file";
  }
else {
  return undef;
  }
}

virtusers_ignore(domain)

This optional function should be implemented by plugins that add and manage email aliases to a domain - for example, one that deals with mailing lists or autoresponders. Because you don't generally want these aliases showing up in the general list of those in the domain, it should return a list of full addresses to hide from the list.

sub virtusers_ignore
{
my ($d) = @_;
return ( "myplugin\@$d->{'dom'}" );
}

Limits and Template Functions

Plugins can define fields that will appear on the owner limits page for a virtual server, and in server templates. Limits are useful if your plugin uses up resources of some kind, such as disk space for databases or memory for server processes. You can then allow the master administrator to define limits on these resources, via functions documented here.

Virtualmin templates are the location of most configuration settings that are used when creating new virtual servers. If your plugin has some adjustable settings that might be used when it is enabled, you can implement the functions below to add new input fields to templates. These can then be fetched in your plugin's feature_setup function with the get_template call.

feature_limits_input(domain)

This optional function should return a HTML inputs for limits specific to this plugin's feature. The initial values of those limits should be take from the domain object, where they must be stored in keys starting with the plugin's name (to avoid clashes). The HTML returned must make use of the ui_table_row function to format table columns.

sub feature_limits_input
{
my ($d) = @_;
if ($d->{$module_name}) {
  return &ui_table_row("Maximum My plugin databases",
    &ui_opt_textbox($module_name."limit", $d->{$module_name."limit"},
                    4, "Unlimited", "At most"));
  }
}

feature_limits_parse(domain, in)

This function parses the HTML form inputs generated by feature_limits_input. It should examine the in hash reference and update the domain object to set or clear limits based on the user's selections. If any errors are found it should return an error message string, or undef if all is OK.

sub feature_limits_parse
{
my($d, $in) = @_;
if (!$d->{$module_name}) {
  # Do nothing
  }
elsif ($in->{$module_name."limit_def"}) {
  delete($d->{$module_name."limit"});
  }
else {
  $in->{$module_name."limit"} =~ /^\d+$/ || return "Limit must be a number";
  $d->{$module_name."limit"} = $in->{$module_name."limit"};
  }
return undef;
}

template_input(template)

This optional function must return HTML for editing template settings specific to this plugin. The template parameter is a hash reference to a template object, which contains settings for all features and plugins. Yours should only show and edit keys that start with the plugin's module name, so that they are properly merged when a non-default template is edited. HTML returned must make use of the ui_table_row function to format table columns.

sub template_input
{
my ($tmpl) = @_;
return &ui_table_row("Default My plugin database size",
  &ui_opt_textbox($module_name."dbsize", $tmpl->{$module_name."dbsize"}, 5, "Default").
  "MB");
}

template_parse(template, in)

This function must check in for selections made by the user in the fields created by template_input, and then update the template hash reference. If there are any errors in the user's input it should return an error string, or undef if everything is OK. Template keys must start with the plugin's module name, so that they are properly merged when a non-default template is edited.

sub template_parse
{
my ($tmpl, $in) = @_;
if ($in->{$module_name."dbsize_def"}) {
  delete($tmpl->{$module_name."dbsize"});
  }
else {
  $in->{$module_name."dbsize"} =~ /^\d+$/ || return "Database size must be a number";
  $tmpl->{$module_name."dbsize"} = $in->{$module_name."dbsize"};
  }
}

Backup and Restore Functions

In the Virtualmin architecture, each feature and plugin is responsible for backing up and restoring configuration files associated with a domain, but which are stored outside the virtual server's home directory. If your plugin adds a feature to Virtualmin which stores data in some location that won't be included in a domain's regular back, you should implement the functions in this section to ensure that it is backed up and restored.

feature_backup(domain, file, opts, all-opts)

This function should copy configuration files associated with the virtual server object domain and copy them to the path given by file. If there is just a single file then it can be copied directly - otherwise, your code should create a tar file of all required files and write it to that path.

The &$virtual_server::first_print and second_print functions should be called to tell the user that the backup is starting, and if it has succeeded or failed. If the copy was successful the function should return 1, or 0 on failure.

sub feature_backup
{
my ($d, $file) = @_;
&$virtual_server::first_print("Copying My plugin configuration file ..");
my $ok = &copy_source_dest("/etc/someprogram/$d->{'dom'}", $file);
if ($ok) {
  &$virtual_server::second_print(".. done");
  return 1;
  }
else {
  &$virtual_server::second_print(".. copy failed!");
  return 0;
  }
}

feature_restore(domain, file, opts, all-opts)

This function is the opposite of feature_backup - it should take the data in the file passed in with the file parameter, and update local config files or databases for the virtual server defined in domain to restore those settings. The format of file will be exactly the same as whatever your plugin created in the feature_backup function, although it may be in a different location.

The &$virtual_server::first_print and second_print functions should be called to tell the user that the restore is starting, and if it has succeeded or failed. If the process was successful the function should return 1, or 0 on failure.

sub feature_restore
{
my ($d, $file) = @_;
&$virtual_server::first_print("Restoring My plugin configuration file ..");
my $ok = &copy_source_dest($file, "/etc/someprogram/$d->{'dom'}");
if ($ok) {
  &$virtual_server::second_print(".. done");
  return 1;
  }
else {
  &$virtual_server::second_print(".. copy failed!");
  return 0;
  }
}

Other User Interface Functions

These functions aren't really related to any feature or capability that the plugin provides - instead, the allow it to add elements to the Virtualmin user interface.

settings_links

If implemented, this function should return a list of hash references, each of which defines a new link under the System Settings menus on Virtualmin's left frame. These are only accessible to the master administrator, and appear regardless of which domain is selected. They typically link to global configuration pages for the plugin.

Each hash must contain the following keys :

sub settings_links
{
return ( { 'link' => "/$module_name/edit_config.cgi",
           'title' => "My plugin configuration",
           'icon' => "/$module_name/images/config.gif",
           'cat' => 'setting' } );
}

theme_sections

The Virtualmin framed theme displays various information on the right-hand system information page after you login, such as the status of servers, available updates and comparative quota use. This function allows your plugin to add sections of its own, typically to display global status information.

If defined, it must return a list of hash references, each containing the following keys :

sub theme_sections
{
return ( { 'title' => 'My plugin status',
           'html' => &is_server_running() ? 'Some program is running OK'
                                          : 'Some program is down!',
           'status' => 0,
           'for_master' => 1 } );
}

Functions For Mailboxes

A Virtualmin plugin can also provide extra capabilities to virtual server users. This is done by implementing additional functions in the virtual_feature.pl file, similar to those used for adding a new server feature. This can be used for granting users access to some new service, like a game server or database, which is not supported natively by Virtualmin.

When a plugin adds capabilities to a user, additional inputs will typically appear on the user editing page. In additional, the plugin can define extra columns to appear in the user list, to display the status of the new user capabilities.

Most of the functions above take a user details hash reference as a parameter. Some of the useful keys in this hash are :

The functions that can be added to virtual_feature.pl to support user capabilities are :

mailbox_inputs(user, new, domain)

This function is called when the page for editing a virtual server user is displayed. The user parameter is a hash reference of user details, such as the login name, real name and home directory. The new parameter will be set to 1 if this is a new user, or 0 if editing an existing user. The domain parameter is a hash reference of virtual server information, as used in the plugin functions documented above.

This function must return HTML for the additional inputs to display, formatted to fit inside a 2-column table. This is best done with functions from ui-lib.pl, like:

sub mailbox_inputs
{
my ($user, $new, $d) = @_;
my $access = &check_user_access($user);
return &ui_table_row("Allow access to My plugin?",
                     &ui_yesno_radio("myplugin", $access));
}

It should detect the current state of the user, and use this information to determine the values of the inputs.

mailbox_validate(user, olduser, in, new, domain)

This function is called when the user form is saved, but before any changes are actually committed. It should check the form inputs in the in hash reference to make sure they are valid, and return either undef on success, or an error message if there is some problem.

mailbox_save(user, olduser, in, new, domain)

This function must save the actual settings selected for this user, by updating whatever configuration files are needed for this capability. The user parameter is the update user details hash, containing his new username, password, real name and other attributes. The olduser parameter is the user hash from before the changes were made, and can be compared with user to detect username and other changes. in is the form inputs hash, new is a flag indicating if this is a new or edited user, and domain is the details of the virtual server this user is in.

sub mailbox_save
{
my ($user, $olduser, $in, $new, $d) = @_;
if ($user->{'user'} ne $olduser->{'user'}) {
  &set_user_access($olduser, 0);
  }
&set_user_access($user, $in->{'myplug'} ? 1 : 0);
}

mailbox_delete(user, domain)

This function is called when a user is deleted. It should check to see if he has the capability managed by this plugin enabled, and if so perform whatever tasks are needed to remove it. The parameters are the same as those for the mailbox_save function.

sub mailbox_save
{
my ($user, $d) = @_;
&set_user_access($user, 0);
}

mailbox_modify(user, olduser, domain)

This function gets called when a user is modified by some part of Virtualmin other than the Edit User page, for example by the modify-user.pl command-line script. It should compare the old and new user objects to see if anything that this plugin uses has changed, such as the username or password. If so, it must update whatever configuration files the plugin uses.

sub mailbox_modify
{
my ($user, $olduser, $d) = @_;
if ($user->{'user'} ne $olduser->{'user'}) {
  my $oldaccess = &get_user_access($olduser);
  &set_user_access($olduser, 0);
  &set_user_access($user, $oldaccess);
  }
}

mailbox_header(domain)

If you want an additional column to appear in the user list indicating the state of this plugin's capability for users, this function should return the title for the column. Otherwise, it should just return undef. If you don't need to define any extra column, then you don't even need to implement it.

sub mailbox_header
{
return "Plugin access";
}

mailbox_column(user, domain)

When a column exists for this plugin in the user list, this function will be called once for each user. It must return the text to display, such as Enabled or Disabled. If mailbox_header is not implemented, then this function doesn't need to be either.

sub mailbox_column
{
local ($user, $d) = @_;
return &check_user_access($user) ? "Yes" : "No";
}

mailbox_defaults_inputs(defs, domain)

Virtualmin Pro allows users to define various defaults for new users added to domains, on a per-domain basis. If your plugin wants to be able to add to these defaults, you can implement this function. The defs parameters is a hash reference for a user object containing the defaults, which should be checked to find the current status for your settings.

sub mailbox_defaults_inputs
{
my ($defs, $d) = @_;
return &ui_table_row("Allow access to My plugin by default?",
                     &ui_yesno_radio("myplugin", $defs->{'myplugin'}));
}

mailbox_defaults_parse(defs, domain, in)

This function is the counterpart to mailbox_defaults_inputs. It should check form inputs in in and use them to update the default settings object defs.


sub mailbox_defaults_parse
{
my ($defs, $d, $in) = @_;
$defs->{'myplugin'} = $in->{'myplugin'};
}

Database Functions

In the core package, Virtualmin supports MySQL and PostgreSQL databases. However, the plugin architecture allows developers to add new database types which can then be associated with virtual servers. Typically a plugin that adds databases will also implement the feature_ functions, so that the new database type can be enabled for new or existing virtual servers - just as is the case for MySQL and PostgreSQL.

Because Virtualmin allows mailbox users to have access to some database types, the plugin can also include support for creating, listing and managing additional users associated with each database. Because not all database systems support granting a user full access to a database, implementation of the user-related functions is optional.

database_name()

This function must return the name of the database type.

sub database_name
{
return "FooSQL";
}

database_list(domain)

This function must return a list of the names of databases owned by the given domain object, each of which is a hash reference containing the following keys :

Typically the list of databases for a domain will be stored in the domain hash itself, in a key named db_$module_name. This removes the need for the plugin to store the domain → database mapping separately.

sub database_list
{
my ($d) = @_;
my @rv;
foreach my $db (split(/\s+/, $d->{'db_'.$module_name})) {
        push(@rv, { 'name' => $db,
                    'type' => $module_name,
                    'desc' => &database_name(),
                    'link' => "/$module_name/edit_dbase.cgi?db=$db" });
        }
return @rv;
}

databases_all()

This function should return a list of all databases known to the database server the plugin manages, even those not associated with any domain. Its return format should be the same as database_list.

sub databases_all
{
my @rv;
foreach my $dbname (&list_foosql_databases()) {
  push(@rv, { 'name' => $dbname,
              'type' => $module_name,
              'desc' => &database_name() });
  }
return @rv;
}

database_clash(domain, name)

This function must check if a database of the type managed by the plugin with the given name already exists, and if so return 1. It is used by Virtualmin to prevent database name collisions at creation time. If no clash exists, it must return 0.

sub database_clash
{
my ($d, $name) = @_;
foreach my $db (&list_foosql_databases()) {
  return 1 if ($db eq $name);
  }
return 0;
}

database_create(domain, name)

This function is where the real work of creating a new database should happen. It must perform all the work needed to add a database and associate it with the virtual server, typically by adding it to the db_$module_name key in the domain hash reference. It should use &$virtual_server::first_print to output a message before creation starts, and second_print to display success or failure when done. It should return 1 if creation was successful, 0 if not.

Access to the new database must be granted to the virtual server's owner. For databases managed by some kind of server (like MySQL and PostgreSQL), the domain's username and password must be able to login to access the new database. These can be found in the domain hash in the user and pass keys.

sub database_create
{
my ($d, $name) = @_;
&$virtual_server::first_print("Creating FooSQL database $name ..");
local $err = &create_foosql_database($name);
if ($err) {
  &$virtual_server::second_print(".. failed : $err");
  return 0;
  }
else {
  &$virtual_server::second_print(".. done");
  $d->{'db_'.$module_name} .= " ".$name;
  return 1;
  }
}

database_delete(domain, name)

This function must delete a database of the type managed by this plugin, and remove access to it from the virtual server. Like database_create, it should use the print functions to display progress and status to the user.


sub database_delete
{
my ($d, $name) = @_;
&$virtual_server::first_print("Deleting FooSQL database $name ..");
local $err = &delete_foosql_database($name);
if ($err) {
  &$virtual_server::second_print(".. failed : $err");
  return 0;
  }
else {
  &$virtual_server::second_print(".. done");
  $d->{'db_'.$module_name} =~ s/\s+\Q$name\E//g;
  return 1;
  }
}

database_size(domain, name)

This function is called by Virtualmin when a user displays information about a database, and when computing a virtual server's total disk usage. It must return two numbers :

sub database_size
{
my ($d, $name) = @_;
my $size = &disk_usage_kb("/var/foosql/$name");
my @tables = &list_foosql_tables($name);
return ( $size*1024, scalar(@tables) );
}

database_users(domain, name)

If the plugin's database type supports multiple logins, this function can be implemented to return a list of array references, each of which contains a login and password. Only users associated with domain and with access to the database specified by the name parameter need to be returned. If the password is encrypted, it is fine to use that as the second element of each array ref.

sub database_users
{
my ($d, $name) = @_;
return &execute_foosql_sql($name, "select login,password from users where db = '$name'");
}

database_create_user(domain, database, user, password)

This function must create a new database with with access to the database specified by the database parameter, which is a hash reference returned by database_list. The new user must have the login set by the user parameter, and password specified by password. If something goes wrong, it should called error.

sub database_create_user
{
my ($d, $db, $user, $pass) = @_;
&execute_foosql_sql($db->{'name'}, "create user '$user' with password '$pass'");
}

database_modify_user(domain, old-database, database, old-user, user, password)

This function must modify the user in the database specified by the old-database parameter and named old-user, changing his login to user and password to password (if provided). If the modification fails, it should call error.

sub database_modify_user
{
my ($db, $olddb, $db, $olduser, $user, $pass) = @_;
if ($user ne $olduser) {
  &execute_foosql_sql($olddb->{'name'}, "rename user '$olduser' to '$user'");
  }
if (defined($pass)) {
  &execute_foosql_sql($olddb->{'name'}, "alter user '$user' password '$pass'");
  }
}

database_delete_user(domain, user)

This function should delete the database user specified by the user parameter from all databases owned by the virtual server in domain.

sub database_delete_user
{
my ($d, $user) = @_;
foreach my $name (&list_foosql_databases()) {
  &execute_foosql_sql($name, "delete user '$user'");
  }
}

database_user(name)

Some database servers impose limits on the length or allowed characters in database logins. This function should check if the given name exceeds any such restrictions, and if so truncate or modify it to be valid. It should then return the modified version.

sub database_user
{
my ($name) = @_;
if (length($name) > 16) {
  $name = substr($name, 0, 16);
  }
return $name;
}