めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Private notes on Packstack plugin development (2)

Global options

I'd like to add my first working plugin which configures "/etc/motd" of the target server. This has three options:

1) A switch to enable/disable the plugin.
2) IP address of the target server. (Default is localhost.)
3) One line message for "/etc/motd". (Default is "Hello, World!".)

Options in Packstack are grouped into two categories: "Global options" and "Plugin(Local?) options". The plugin options are used to set parameters to a specific plugin. In this case (1) is in global options and (2)(3) are in plugin options. Users can specify these options by one of the three ways:

1) Command line options to packstack command.
2) Interactive input as you have seen in the first run.
3) Answer file, explained later.

Let's focus on the global options in this section. They are defined in "prescript_000.py" plugin. Let's add the following block to it.

plugins/prescript_000.py

    paramsList = [
                  {"CMD_OPTION"      : "ntp-severs",
...
                   "USE_DEFAULT"     : False,
                   "NEED_CONFIRM"    : False,
                   "CONDITION"       : False },
### Add from here.
                  {"CMD_OPTION"      : "config-motd",
                   "USAGE"           : "Set to 'y' if you would like Packstack to configure motd",
                   "PROMPT"          : "Should Packstack configure motd",
                   "OPTION_LIST"     : ["y", "n"],
                   "VALIDATORS"      : [validators.validate_options],
                   "DEFAULT_VALUE"   : "y",  
                   "MASK_INPUT"      : False,
                   "LOOSE_VALIDATION": False,
                   "CONF_NAME"       : "CONFIG_MOTD",
                   "USE_DEFAULT"     : False,
                   "NEED_CONFIRM"    : False,
                   "CONDITION"       : False },
### Add until here.
                 ] 

Here is the explanation for some important parameters.

Parameter Explanation
CMD_OPTION Command line option name
USAGE Usage message
PROMPT Message for interactive input
OPTION_LIST List of possible values
VALIDATORS Validator methods
CONF_NAME Key of the configuration parameter hash "controller.CONF"

CMD_OPTION becomes the name of command line option of the "packstack" command. USAGE and PROMPT are used for the interactive input. OPTION_LIST and VALIDATORS are used to restrict the possible parameter values. OPTION_LIST can be an empty list "[]" if you want to allow arbitrary value. But you can still use validators to restrict the possible values. Validators are defined in "installer/validators.py". The following is the list of validators. You may speculate what they do from their names. Please see the source code for details.

installer/validators.py

__all__ = ('ParamValidationError', 'validate_integer', 'validate_float',
           'validate_regexp', 'validate_port', 'validate_not_empty',
           'validate_options', 'validate_ip', 'validate_multi_ip',
           'validate_file', 'validate_ping', 'validate_ssh',
           'validate_multi_ssh')

The value of options can be referred through the hash "controller.CONF" in the code. CONF_NAME is the key of the hash.

There is an important convention for CONF_NAME. If it ends with "_HOST", the option is interpreted as specifying the target server. If it ends with "_HOSTS", it is interpreted as specifying the list of target servers. gethostlist(config) (defined in "modules/ospluginutils.py") returns the list of values in these options. For example, "sshkeys_000.py" plugin tries to set public key authentication for servers in that list as below:

plugin/sshkeys_000.py

def installKeys():
    with open(controller.CONF["CONFIG_SSH_KEY"]) as fp:
        sshkeydata = fp.read().strip()
    for hostname in gethostlist(controller.CONF):
        if '/' in hostname:
            hostname = hostname.split('/')[0]
        server = utils.ScriptRunner(hostname)
        # TODO replace all that with ssh-copy-id
        server.append("mkdir -p ~/.ssh")
        server.append("chmod 500 ~/.ssh")
        server.append("grep '%s' ~/.ssh/authorized_keys > /dev/null 2>&1 || echo %s >> ~/.ssh/authorized_keys" % (sshkeydata, sshkeydata))
        server.append("chmod 400 ~/.ssh/authorized_keys")
        server.append("restorecon -r ~/.ssh")
        server.execute()

Plugin options

Plugin options are defined in the plugin module itself. Now let's replace the NOOP plugin with the new one "motd_020.py".

# cd /usr/lib/python2.*/site-packages/packstack
# rm plugin/noop_010.py

Create the following plugin module "plugin/motd_020.py":

plugin/motd_020.py

"""
plugin for configuring /etc/motd
"""

import os
import logging

from packstack.installer import validators
import packstack.installer.common_utils as utils
from packstack.installer.exceptions import ScriptRuntimeError

from packstack.modules.ospluginutils import getManifestTemplate, appendManifestFile, manifestfiles

# Controller object will be initialized from main flow
controller = None

PLUGIN_NAME = "OS-MOTD"

logging.debug("plugin %s loaded", __name__)

def initConfig(controllerObject):
    global controller
    controller = controllerObject

    paramsList = [
                  {"CMD_OPTION"      : "motd-host",
                   "USAGE"           : "The IP address of the server to configure its motd",
                   "PROMPT"          : "Enter the IP address of the target server",
                   "OPTION_LIST"     : [],
                   "VALIDATORS"      : [validators.validate_ip, validators.validate_ssh],
                   "DEFAULT_VALUE"   : utils.getLocalhostIP(),
                   "MASK_INPUT"      : False,
                   "LOOSE_VALIDATION": True,
                   "CONF_NAME"       : "CONFIG_MOTD_HOST",
                   "USE_DEFAULT"     : False,
                   "NEED_CONFIRM"    : False,
                   "CONDITION"       : False },
                  {"CMD_OPTION"      : "motd-message",
                   "USAGE"           : "message text in /etc/motd",
                   "PROMPT"          : "Enter the message",
                   "OPTION_LIST"     : [],
                   "VALIDATORS"      : [validators.validate_not_empty],
                   "DEFAULT_VALUE"   : "Hello, World!",
                   "MASK_INPUT"      : False,
                   "LOOSE_VALIDATION": True,
                   "CONF_NAME"       : "CONFIG_MOTD_MESSAGE",
                   "USE_DEFAULT"     : False,
                   "NEED_CONFIRM"    : False,
                   "CONDITION"       : False },
                 ]
    groupDict = { "GROUP_NAME"            : "MOTD",
                  "DESCRIPTION"           : "MOTD Options",
                  "PRE_CONDITION"         : "CONFIG_MOTD",
                  "PRE_CONDITION_MATCH"   : "y",
                  "POST_CONDITION"        : False,
                  "POST_CONDITION_MATCH"  : True}
    controller.addGroup(groupDict, paramsList)

def initSequences(controller):
    if controller.CONF['CONFIG_MOTD'] != 'y':
        return

    configmotdsteps = [
             {'title': 'Configuring /etd/motd message', 'functions':[createmotdmanifest]},
    ]
    controller.addSequence("Installing config-motd Component", [], [], configmotdsteps)

def createmotdmanifest():
    manifestfile = "%s_motd.pp" % controller.CONF['CONFIG_MOTD_HOST']
    manifestdata = getManifestTemplate("motd.pp")
    appendManifestFile(manifestfile, manifestdata, 'motd')

Note: You may now wondering about the naming convention of the plugin files. The number at the end of the file name indicates the execution order of plugins.

A valid plugin must implement a function initConfig() as above, and "paramList" specifies the plugin options there. Parameters for each option are basically the same as global options. There are two options here:

1) CONFIG_MOTD_HOST: IP address of the target server.
2) CONFIG_MOTD_MESSAGE: Message string in /etc/motd.

"groupdict" is used to groping the plugin parameters of this plugin. "PRE_CONDITION" and "PRE_CONDITION_MATCH" is used to specify when these options are available. In this case:

                  "PRE_CONDITION"         : "CONFIG_MOTD",
                  "PRE_CONDITION_MATCH"   : "y",

When global option "CONFIG_MOTD" is set to "y", these plugin parameters become available and a user is requested to input them at run time unless they are specified in command line options or the answer file.

Worker queue

As another convention, a valid plugin must implement a function initSequences() as in the above example, too. In this function, plugin adds functions to the worker queue. After all plugins have been evaluated, the functions in the queue are sequentially (with FIFO style) executed.

In this case, the plugin adds function createmanifest() with controller.addSequence method as below:

def initSequences(controller):
    if controller.CONF['CONFIG_MOTD'] != 'y':
        return

    configmotdsteps = [
             {'title': 'Configuring /etd/motd message', 'functions':[createtestmanifest]},
    ]
    controller.addSequence("Installing config-motd Component", [], [], configmotdsteps)

def createtestmanifest():
    manifestfile = "%s_motd.pp" % controller.CONF['CONFIG_MOTD_HOST']
    manifestdata = getManifestTemplate("motd.pp")
    appendManifestFile(manifestfile, manifestdata)

In tern, createmanifest() prepares a Puppet manifest from the specified template in the later sequential execution phase. This is where Packstack meets Puppet. This integration mechanism with Puppet is the next topic.

Manifest templates

createmanifest() in the previous example does three things:

1) Construct manifest file name in the formart: _templatename.pp
2) Fetch manifest file from the template directory "puppet/templates" via getManifestTemplate().
3) Put the fetched manifest in the staging area with the filename constructed at (1).

There are some tricks here. The manifests placed in the staging area will be applied to the target server indicated by the head of its filename in the later stage when applyPuppetManifest() in the worker queue (which is added by the plugin puppet_950.py) is executed.

Ahhh... It's complicated. The following drawing shows the whole picture:


The actual location of staging area is under the log directory "/var/tmp/packstack/*/manifest/".

Anyway, what you have to do is just to fetch the manifest template and place it in the staging area with an appropriate filename. As a preparation, you need to create the template motd.pp in the template directory puppet/templates as below:

puppet/templates/motd.pp

file {'motd':
    path => '/etc/motd',
    ensure => file,
    mode => '0644',
    content => "%(CONFIG_MOTD_MESSAGE)s\n",
}

When copied to the staging area, "%(KEY)s" is replaced with the configuration option "controller.CONF['KEY']". This trick comes from the following part:

modules/ospluginutils.py

def getManifestTemplate(template_name):
    with open(os.path.join(PUPPET_TEMPLATE_DIR, template_name)) as fp:
        return fp.read() % controller.CONF

Python's formatting operator "%" is applied with the hash "controller.CONF" which contains the configuration options. In this case, the content of /etc/motd is replaced with the CONFIG_MOTD_MESSAGE which was defined as a plugin option in "plugin/motd_020.py".

Using packstack for remote servers

Now we're ready to try the new plugin "plugin/motd_020.py". To see the real power of Packstack, I will apply this plugin against remote server. Whereas the localhost is 192.168.122.191, I specify 192.168.122.192 as the IP address of the target server.

# packstack 
Welcome to Installer setup utility
Enter the path to your ssh Public key to install on servers  [/root/.ssh/id_rsa.pub] : 
Enter a comma separated list of NTP server(s). Leave plain if Packstack should not install ntpd on instances.: 192.168.122.1
Should Packstack configure motd [y|n]  [y] : 
Enter the IP address of the target server  [192.168.122.191] : 192.168.122.192
Enter the message  [Hello, World!] : 
To subscribe each server to EPEL enter "y" [y|n]  [y] : 
Enter a comma separated list of URLs to any additional yum repositories to install: 
To subscribe each server to Red Hat enter a username here: 
To subscribe each server to Red Hat enter your password here :
To subscribe each server to Red Hat Enterprise Linux 6 Server Beta channel (only needed for Preview versions of RHOS) enter "y" [y|n]  [n] : 
To subscribe each server with RHN Satellite enter RHN Satellite server URL: 

Installer will be installed using the following configuration:
==============================================================
ssh-public-key:                /root/.ssh/id_rsa.pub
ntp-severs:                    192.168.122.1
config-motd:                   y
motd-host:                     192.168.122.192
motd-message:                  Hello, World!
use-epel:                      y
additional-repo:               
rh-username:                   
rh-password:                   
rh-beta-repo:                  n
rhn-satellite-server:          
Proceed with the configuration listed above? (yes|no): yes

Installing:
Clean Up...                                              [ DONE ]
Setting up ssh keys...root@192.168.122.192's password: 
                                   [ DONE ]
Adding pre install manifest entries...                   [ DONE ]
Installing time synchronization via NTP...               [ DONE ]
Configuring /etd/motd message...                         [ DONE ]
Preparing servers...                                     [ DONE ]
Adding post install manifest entries...                  [ DONE ]
Installing Dependencies...                               [ DONE ]
Copying Puppet modules and manifests...                  [ DONE ]
Applying Puppet manifests...
Applying 192.168.122.192_prescript.pp
192.168.122.192_prescript.pp :                                       [ DONE ]
Applying 192.168.122.192_ntpd.pp
192.168.122.192_ntpd.pp :                                            [ DONE ]
Applying 192.168.122.192_motd.pp
192.168.122.192_motd.pp :                                            [ DONE ]
Applying 192.168.122.192_postscript.pp
192.168.122.192_postscript.pp :                                      [ DONE ]
                            [ DONE ]

 **** Installation completed successfully ******

Additional information:
 * A new answerfile was created in: /root/packstack-answers-20130520-164008.txt
 * The installation log file is available at: /var/tmp/packstack/20130520-163946-32gw8d/openstack-setup.log

Let's see the /etc/motd of the target server.

# ssh root@192.168.122.192
The authenticity of host '192.168.122.192 (192.168.122.192)' can't be established.
RSA key fingerprint is 89:88:fd:02:14:1e:33:bc:68:97:ee:30:bc:ac:49:ff.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.122.192' (RSA) to the list of known hosts.
Hello, World!

Well-done :)

Continue to the next part.