This is part one of a series of posts of extending apnscp to create new service definitions. Service definitions extend software accessibility and panel access, which allows for detailed panel access rights.


We'll look at providing Java support to accounts, which can be heavy on memory, so certain controls are necessary to restrict which accounts can use it.

Note, that this does not prevent an account from downloading the Java binary directly; disabling ssh and setting low memory limits via cgroup,memory is your best deterrent to stymie such abuse.

Service definition definition

Service Definition: metadata that describe what an account is, what features it has, and how it should be handled through code.

Examples of metadata include the administrative login (siteinfo,admin_user), database access (mysql,enabled and/or pgsql,enabled), billing identifier (billing,invoice), addon domains (aliases,aliases), or even extended filesystem layers conferred through service enablement, such as (ssh,enabled) that merges command-line access into an account.

All builtin derivable Service Definitions exist as templates in plans/.skeleton. Any definition beyond that may be extended, just don't override these definitions and read along!

A new plan can be created using Artisan.

cd /usr/local/apnscp
./artisan opcenter:plan --new java

All files from resources/plans/.skeleton will be copied into resources/plans/java. Do not edit .skeleton; 10,000v to the nipples shall ensue for violators.

A base plan can be assigned to a site using -p or --plan,

AddDomain -p java -c siteinfo,domain=newdomain.com -c siteinfo,admin_user=myadmin

Moreover, the default plan can be changed using Artisan.

./artisan opcenter:plan --default java

Now specifying -p or --plan is implied for site creation.

Mapped services

A mapped service connects metadata to apnscp through a Service Validator. A Service Validator can accept or reject a sequence of changes determined upon the return value (or trigger of error via error()). Service Validators exist in two forms, as modules through _verify_conf listed above and classes that reject early by implementing ServiceValidator; such objects are mapped services.

Definition behaviors

Certain services can be triggered automatically when a service value is toggled or modified.

Certain services can be triggered automatically when a service value is toggled or modified.

MountableLayer

MountableLayer may only be used on "enabled" validators. When enabled, a corresponding read-only filesystem hierarchy under /home/virtual/FILESYSTEMTEMPLATE is merged into the account's filesystem.

ServiceReconfiguration

Implements reconfigure() and rollback() methods, which consists of the old and new values on edit. On failure rollback() is called. Rollback is not compulsory.

AlwaysValidate

Whenever a site is created or edited, if a service definition implements AlwaysValidate, then the valid($value) will be invoked to ensure changes conform to the recommended changes. Returning FALSE will halt further execution and perform a rollback through depopulate() of any previously registered and confirmed services.

ServiceInstall

ServiceInstall implements populate() and depopulate() methods. These are called when the supplied $value is true (true, 1, any string of 1 or more characters). It can be used to initially provision a site as well (as with ipinfo,nbaddrs and ipinfo,ipaddrs when namebased is toggled).

AlwaysRun

Service Definitions that implement AlwaysRun invert the meaning of ServiceLayer. populate() is now called whenever its configuration is edited or on account creation and depopulate() is called when an account is deleted or an edit fails. It can be mixed with AlwaysValidate to always run whenever a service value from the service is modified. An example is creating the maildir folder hierarchy. version is always checked (AlwaysValidate interface) and Mail/Version will always create mail folder layout regardless mail,enabled is set to 1 or 0.

Sample service

Service definitions map account metadata to application logic. First, let's look at a hypothetical service  example that enabled Java for an account.

[java]
version=3.0 ; apnscp service version, tied to panel. Must be 3.0.
enabled=1   ; 1 or 0, mounts the filesystem layer
services=[] ; arbitrary list of permitted services implemented by module

This service lives in resources/plans/java/java where the first java is the plan name and second, service named java.

Validating service configuration

Let's create a validator for enabled and services configuration values.

Opcenter\Validators\Java\Enabled.php

<?php declare(strict_types=1);
    /**
     * Simple Java validator
     */

    namespace Opcenter\Service\Validators\Java;

    use Opcenter\SiteConfiguration;
    use Opcenter\Service\Contracts\MountableLayer;
    use Opcenter\Service\Contracts\ServiceInstall;

    class Enabled extends \Opcenter\Service\Validators\Common\Enabled implements MountableLayer, ServiceInstall
    {
        use \FilesystemPathTrait;
           
        /**
         * Validate service value
         */
        public function valid(&$value): bool
        {
            if ($value && !$this->ctx->getServiceValue('ssh','enabled')) {
                return error('Java requires SSH to be enabled');
            }
            
            return parent::valid($value);
        }
        
        /**
         * Mount filesystem, install users
         *
         * @param SiteConfiguration $svc
         * @return bool
         */
        public function populate(SiteConfiguration $svc): bool
        {
			return true;
        }

        /**
         * Unmount filesytem, remove configuration
         * 
         * @param SiteConfiguration $svc
         * @return bool
         */
        public function depopulate(SiteConfiguration $svc): bool
        {
	        return true;
        }
	}

$this->ctx['var'] allows access to other configuration values within the scope of this service. $this->ctx->getOldServiceValue($svc, $var) returns the previous service value in $svc while $this->ctx->getNewServiceValue($svc, $var) gets the new service value if it is set.

MountableLayer requires a separate directory in FILESYSTEMTEMPLATE/ named after the service. This is a read-only layer that is shared across all accounts that have the service enabled. apnscp ships with siteinfo and ssh service layers which provide basic infrastructure.

Service mounts

Service mounts are part of MountableLayer. Create a new mount named "java" in /home/virtual/FILESYSTEMTEMPLATE. You can optionally install RPMs using Yum Synchronizer, which tracks and replicates RPM upgrades automatically.

mkdir /home/virtual/FILESYSTEMTEMPLATE/java
cd /usr/local/apnscp/bin
./yum-post.php install -d java-1.8.0-openjdk java

-d is a special flag that will resolve dependencies when replicating a package and ensure that those corresponding packages appear in at least 1 service definition. When dependencies or co-dependencies will be noted as an INFO level during setup. When -d is omitted only the specified package will be installed.

Opcenter\Validators\Java\Services.php

<?php declare(strict_types=1);

    namespace Opcenter\Service\Validators\Java;

    use Opcenter\Service\ServiceValidator;

    class Services extends ServiceValidator
    {
        const DESCRIPTION = 'Enable Java-based services';
        const VALUE_RANGE = ['tomcat','jboss'];

        public function valid(&$value): bool
        {
            if ($value && !$this->ctx['enabled']) {
                warn("Disabling services - java disabled");
                $value = [];
            } else if (!$value) {
                // ensure type is array
                $value = [];
                return true;
            }
            if (!$value) {
                $value = [];
            }
            
            // prune duplicate values
            $value = array_unique($value);
            
            foreach ($value as $v) {
                if (!\in_array($v, self::VALUE_RANGE, true)) {
                    return error("Unknown Java service `%s'", $v);
                }
            }

            return true;
        }
    }
By convention, variables that triggered an error are enclosed within `' or prefixed with ":" for multi-line responses, such as stderr. While not mandatory, it helps call out the value that yielded the error and when the variable is omitted rather verbosely.

To keep things simple for now, the service validator checks if the services configured are tomcat or jboss. We'll tie this logic back into the "enabled" service validator. For now to enable this service is simple:

EditDomain -c java,enabled=1 -c java,services=['tomcat'] -D mydomain.com

Stay tuned for the next installment, which looks at building a module for Java.