saltstack/salt

View on GitHub
salt/modules/zonecfg.py

Summary

Maintainability
F
2 wks
Test Coverage
# -*- coding: utf-8 -*-
'''
Module for Solaris 10's zonecfg

:maintainer:    Jorge Schrauwen <sjorge@blackdot.be>
:maturity:      new
:platform:      OmniOS,OpenIndiana,SmartOS,OpenSolaris,Solaris 10
:depend:        salt.modules.file

.. versionadded:: 2017.7.0

.. warning::
    Oracle Solaris 11's zonecfg is not supported by this module!
'''
from __future__ import absolute_import, print_function, unicode_literals

# Import Python libs
import logging
import re

# Import Salt libs
import salt.utils.args
import salt.utils.data
import salt.utils.decorators
import salt.utils.files
import salt.utils.path
from salt.utils.odict import OrderedDict

# Import 3rd-party libs
from salt.ext import six

log = logging.getLogger(__name__)

# Define the module's virtual name
__virtualname__ = 'zonecfg'

# Function aliases
__func_alias__ = {
    'import_': 'import'
}

# Global data
_zonecfg_info_resources = [
    'rctl',
    'net',
    'fs',
    'device',
    'dedicated-cpu',
    'dataset',
    'attr',
]

_zonecfg_info_resources_calculated = [
    'capped-cpu',
    'capped-memory',
]

_zonecfg_resource_setters = {
    'fs': ['dir', 'special', 'raw', 'type', 'options'],
    'net': ['address', 'allowed-address', 'global-nic', 'mac-addr', 'physical', 'property', 'vlan-id', 'defrouter'],
    'device': ['match', 'property'],
    'rctl': ['name', 'value'],
    'attr': ['name', 'type', 'value'],
    'dataset': ['name'],
    'dedicated-cpu': ['ncpus', 'importance'],
    'capped-cpu': ['ncpus'],
    'capped-memory': ['physical', 'swap', 'locked'],
    'admin': ['user', 'auths'],
}

_zonecfg_resource_default_selectors = {
    'fs': 'dir',
    'net': 'mac-addr',
    'device': 'match',
    'rctl': 'name',
    'attr': 'name',
    'dataset': 'name',
    'admin': 'user',
}


@salt.utils.decorators.memoize
def _is_globalzone():
    '''
    Check if we are running in the globalzone
    '''
    if not __grains__.get('kernel') == 'SunOS':
        return False

    zonename = __salt__['cmd.run_all']('zonename')
    if zonename['retcode']:
        return False
    if zonename['stdout'] == 'global':
        return True

    return False


def __virtual__():
    '''
    We are available if we are have zonecfg and are the global zone on
    Solaris 10, OmniOS, OpenIndiana, OpenSolaris, or Smartos.
    '''
    if _is_globalzone() and salt.utils.path.which('zonecfg'):
        if __grains__['os'] in ['OpenSolaris', 'SmartOS', 'OmniOS', 'OpenIndiana']:
            return __virtualname__
        elif __grains__['os'] == 'Oracle Solaris' and int(__grains__['osmajorrelease']) == 10:
            return __virtualname__
    return (
        False,
        '{0} module can only be loaded in a solaris globalzone.'.format(
            __virtualname__
        )
    )


def _clean_message(message):
    '''Internal helper to sanitize message output'''
    message = message.replace('zonecfg: ', '')
    message = message.splitlines()
    for line in message:
        if line.startswith('On line'):
            message.remove(line)
    return "\n".join(message)


def _parse_value(value):
    '''Internal helper for parsing configuration values into python values'''
    if isinstance(value, bool):
        return 'true' if value else 'false'
    elif isinstance(value, six.string_types):
        # parse compacted notation to dict
        listparser = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''')

        value = value.strip()
        if value.startswith('[') and value.endswith(']'):
            return listparser.split(value[1:-1])[1::2]
        elif value.startswith('(') and value.endswith(')'):
            rval = {}
            for pair in listparser.split(value[1:-1])[1::2]:
                pair = pair.split('=')
                if '"' in pair[1]:
                    pair[1] = pair[1].replace('"', '')
                if pair[1].isdigit():
                    rval[pair[0]] = int(pair[1])
                elif pair[1] == 'true':
                    rval[pair[0]] = True
                elif pair[1] == 'false':
                    rval[pair[0]] = False
                else:
                    rval[pair[0]] = pair[1]
            return rval
        else:
            if '"' in value:
                value = value.replace('"', '')
            if value.isdigit():
                return int(value)
            elif value == 'true':
                return True
            elif value == 'false':
                return False
            else:
                return value
    else:
        return value


def _sanitize_value(value):
    '''Internal helper for converting pythonic values to configuration file values'''
    # dump dict into compated
    if isinstance(value, dict):
        new_value = []
        new_value.append('(')
        for k, v in value.items():
            new_value.append(k)
            new_value.append('=')
            new_value.append(v)
            new_value.append(',')
        new_value.append(')')
        return "".join(six.text_type(v) for v in new_value).replace(',)', ')')
    elif isinstance(value, list):
        new_value = []
        new_value.append('(')
        for item in value:
            if isinstance(item, OrderedDict):
                item = dict(item)
                for k, v in item.items():
                    new_value.append(k)
                    new_value.append('=')
                    new_value.append(v)
            else:
                new_value.append(item)
            new_value.append(',')
        new_value.append(')')
        return "".join(six.text_type(v) for v in new_value).replace(',)', ')')
    else:
        # note: we can't use shelx or pipes quote here because it makes zonecfg barf
        return '"{0}"'.format(value) if ' ' in value else value


def _dump_cfg(cfg_file):
    '''Internal helper for debugging cfg files'''
    if __salt__['file.file_exists'](cfg_file):
        with salt.utils.files.fopen(cfg_file, 'r') as fp_:
            log.debug(
                "zonecfg - configuration file:\n%s",
                    "".join(salt.utils.data.decode(fp_.readlines()))
            )


def create(zone, brand, zonepath, force=False):
    '''
    Create an in-memory configuration for the specified zone.

    zone : string
        name of zone
    brand : string
        brand name
    zonepath : string
        path of zone
    force : boolean
        overwrite configuration

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.create deathscythe ipkg /zones/deathscythe
    '''
    ret = {'status': True}

    # write config
    cfg_file = salt.utils.files.mkstemp()
    with salt.utils.files.fpopen(cfg_file, 'w+', mode=0o600) as fp_:
        fp_.write("create -b -F\n" if force else "create -b\n")
        fp_.write("set brand={0}\n".format(_sanitize_value(brand)))
        fp_.write("set zonepath={0}\n".format(_sanitize_value(zonepath)))

    # create
    if not __salt__['file.directory_exists'](zonepath):
        __salt__['file.makedirs_perms'](zonepath if zonepath[-1] == '/' else '{0}/'.format(zonepath), mode='0700')

    _dump_cfg(cfg_file)
    res = __salt__['cmd.run_all']('zonecfg -z {zone} -f {cfg}'.format(
        zone=zone,
        cfg=cfg_file,
    ))
    ret['status'] = res['retcode'] == 0
    ret['message'] = res['stdout'] if ret['status'] else res['stderr']
    if ret['message'] == '':
        del ret['message']
    else:
        ret['message'] = _clean_message(ret['message'])

    # cleanup config file
    if __salt__['file.file_exists'](cfg_file):
        __salt__['file.remove'](cfg_file)

    return ret


def create_from_template(zone, template):
    '''
    Create an in-memory configuration from a template for the specified zone.

    zone : string
        name of zone
    template : string
        name of template

    .. warning::
        existing config will be overwritten!

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.create_from_template leo tallgeese
    '''
    ret = {'status': True}

    # create from template
    _dump_cfg(template)
    res = __salt__['cmd.run_all']('zonecfg -z {zone} create -t {tmpl} -F'.format(
        zone=zone,
        tmpl=template,
    ))
    ret['status'] = res['retcode'] == 0
    ret['message'] = res['stdout'] if ret['status'] else res['stderr']
    if ret['message'] == '':
        del ret['message']
    else:
        ret['message'] = _clean_message(ret['message'])

    return ret


def delete(zone):
    '''
    Delete the specified configuration from memory and stable storage.

    zone : string
        name of zone

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.delete epyon
    '''
    ret = {'status': True}

    # delete zone
    res = __salt__['cmd.run_all']('zonecfg -z {zone} delete -F'.format(
        zone=zone,
    ))
    ret['status'] = res['retcode'] == 0
    ret['message'] = res['stdout'] if ret['status'] else res['stderr']
    if ret['message'] == '':
        del ret['message']
    else:
        ret['message'] = _clean_message(ret['message'])

    return ret


def export(zone, path=None):
    '''
    Export the configuration from memory to stable storage.

    zone : string
        name of zone
    path : string
        path of file to export to

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.export epyon
        salt '*' zonecfg.export epyon /zones/epyon.cfg
    '''
    ret = {'status': True}

    # export zone
    res = __salt__['cmd.run_all']('zonecfg -z {zone} export{path}'.format(
        zone=zone,
        path=' -f {0}'.format(path) if path else '',
    ))
    ret['status'] = res['retcode'] == 0
    ret['message'] = res['stdout'] if ret['status'] else res['stderr']
    if ret['message'] == '':
        del ret['message']
    else:
        ret['message'] = _clean_message(ret['message'])

    return ret


def import_(zone, path):
    '''
    Import the configuration to memory from stable storage.

    zone : string
        name of zone
    path : string
        path of file to export to

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.import epyon /zones/epyon.cfg
    '''
    ret = {'status': True}

    # create from file
    _dump_cfg(path)
    res = __salt__['cmd.run_all']('zonecfg -z {zone} -f {path}'.format(
        zone=zone,
        path=path,
    ))
    ret['status'] = res['retcode'] == 0
    ret['message'] = res['stdout'] if ret['status'] else res['stderr']
    if ret['message'] == '':
        del ret['message']
    else:
        ret['message'] = _clean_message(ret['message'])

    return ret


def _property(methode, zone, key, value):
    '''
    internal handler for set and clear_property

    methode : string
        either set, add, or clear
    zone : string
        name of zone
    key : string
        name of property
    value : string
        value of property

    '''
    ret = {'status': True}

    # generate update script
    cfg_file = None
    if methode not in ['set', 'clear']:
        ret['status'] = False
        ret['message'] = 'unkown methode {0}!'.format(methode)
    else:
        cfg_file = salt.utils.files.mkstemp()
        with salt.utils.files.fpopen(cfg_file, 'w+', mode=0o600) as fp_:
            if methode == 'set':
                if isinstance(value, dict) or isinstance(value, list):
                    value = _sanitize_value(value)
                value = six.text_type(value).lower() if isinstance(value, bool) else six.text_type(value)
                fp_.write("{0} {1}={2}\n".format(methode, key, _sanitize_value(value)))
            elif methode == 'clear':
                fp_.write("{0} {1}\n".format(methode, key))

    # update property
    if cfg_file:
        _dump_cfg(cfg_file)
        res = __salt__['cmd.run_all']('zonecfg -z {zone} -f {path}'.format(
            zone=zone,
            path=cfg_file,
        ))
        ret['status'] = res['retcode'] == 0
        ret['message'] = res['stdout'] if ret['status'] else res['stderr']
        if ret['message'] == '':
            del ret['message']
        else:
            ret['message'] = _clean_message(ret['message'])

        # cleanup config file
        if __salt__['file.file_exists'](cfg_file):
            __salt__['file.remove'](cfg_file)

    return ret


def set_property(zone, key, value):
    '''
    Set a property

    zone : string
        name of zone
    key : string
        name of property
    value : string
        value of property

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.set_property deathscythe cpu-shares 100
    '''
    return _property(
        'set',
        zone,
        key,
        value,
    )


def clear_property(zone, key):
    '''
    Clear a property

    zone : string
        name of zone
    key : string
        name of property

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.clear_property deathscythe cpu-shares
    '''
    return _property(
        'clear',
        zone,
        key,
        None,
    )


def _resource(methode, zone, resource_type, resource_selector, **kwargs):
    '''
    internal resource hanlder

    methode : string
        add or update
    zone : string
        name of zone
    resource_type : string
        type of resource
    resource_selector : string
        unique resource identifier
    **kwargs : string|int|...
        resource properties

    '''
    ret = {'status': True}

    # parse kwargs
    kwargs = salt.utils.args.clean_kwargs(**kwargs)
    for k in kwargs:
        if isinstance(kwargs[k], dict) or isinstance(kwargs[k], list):
            kwargs[k] = _sanitize_value(kwargs[k])
    if methode not in ['add', 'update']:
        ret['status'] = False
        ret['message'] = 'unknown methode {0}'.format(methode)
        return ret
    if methode in ['update'] and resource_selector and resource_selector not in kwargs:
        ret['status'] = False
        ret['message'] = 'resource selector {0} not found in parameters'.format(resource_selector)
        return ret

    # generate update script
    cfg_file = salt.utils.files.mkstemp()
    with salt.utils.files.fpopen(cfg_file, 'w+', mode=0o600) as fp_:
        if methode in ['add']:
            fp_.write("add {0}\n".format(resource_type))
        elif methode in ['update']:
            if resource_selector:
                value = kwargs[resource_selector]
                if isinstance(value, dict) or isinstance(value, list):
                    value = _sanitize_value(value)
                value = six.text_type(value).lower() if isinstance(value, bool) else six.text_type(value)
                fp_.write("select {0} {1}={2}\n".format(resource_type, resource_selector, _sanitize_value(value)))
            else:
                fp_.write("select {0}\n".format(resource_type))
        for k, v in six.iteritems(kwargs):
            if methode in ['update'] and k == resource_selector:
                continue
            if isinstance(v, dict) or isinstance(v, list):
                value = _sanitize_value(value)
            value = six.text_type(v).lower() if isinstance(v, bool) else six.text_type(v)
            if k in _zonecfg_resource_setters[resource_type]:
                fp_.write("set {0}={1}\n".format(k, _sanitize_value(value)))
            else:
                fp_.write("add {0} {1}\n".format(k, _sanitize_value(value)))
        fp_.write("end\n")

    # update property
    if cfg_file:
        _dump_cfg(cfg_file)
        res = __salt__['cmd.run_all']('zonecfg -z {zone} -f {path}'.format(
            zone=zone,
            path=cfg_file,
        ))
        ret['status'] = res['retcode'] == 0
        ret['message'] = res['stdout'] if ret['status'] else res['stderr']
        if ret['message'] == '':
            del ret['message']
        else:
            ret['message'] = _clean_message(ret['message'])

        # cleanup config file
        if __salt__['file.file_exists'](cfg_file):
            __salt__['file.remove'](cfg_file)

    return ret


def add_resource(zone, resource_type, **kwargs):
    '''
    Add a resource

    zone : string
        name of zone
    resource_type : string
        type of resource
    kwargs : string|int|...
        resource properties

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.add_resource tallgeese rctl name=zone.max-locked-memory value='(priv=privileged,limit=33554432,action=deny)'
    '''
    return _resource('add', zone, resource_type, None, **kwargs)


def update_resource(zone, resource_type, resource_selector, **kwargs):
    '''
    Add a resource

    zone : string
        name of zone
    resource_type : string
        type of resource
    resource_selector : string
        unique resource identifier
    kwargs : string|int|...
        resource properties

    .. note::
        Set resource_selector to None for resource that do not require one.

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.update_resource tallgeese rctl name name=zone.max-locked-memory value='(priv=privileged,limit=33554432,action=deny)'
    '''
    return _resource('update', zone, resource_type, resource_selector, **kwargs)


def remove_resource(zone, resource_type, resource_key, resource_value):
    '''
    Remove a resource

    zone : string
        name of zone
    resource_type : string
        type of resource
    resource_key : string
        key for resource selection
    resource_value : string
        value for resource selection

    .. note::
        Set resource_selector to None for resource that do not require one.

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.remove_resource tallgeese rctl name zone.max-locked-memory
    '''
    ret = {'status': True}

    # generate update script
    cfg_file = salt.utils.files.mkstemp()
    with salt.utils.files.fpopen(cfg_file, 'w+', mode=0o600) as fp_:
        if resource_key:
            fp_.write("remove {0} {1}={2}\n".format(resource_type, resource_key, _sanitize_value(resource_value)))
        else:
            fp_.write("remove {0}\n".format(resource_type))

    # update property
    if cfg_file:
        _dump_cfg(cfg_file)
        res = __salt__['cmd.run_all']('zonecfg -z {zone} -f {path}'.format(
            zone=zone,
            path=cfg_file,
        ))
        ret['status'] = res['retcode'] == 0
        ret['message'] = res['stdout'] if ret['status'] else res['stderr']
        if ret['message'] == '':
            del ret['message']
        else:
            ret['message'] = _clean_message(ret['message'])

        # cleanup config file
        if __salt__['file.file_exists'](cfg_file):
            __salt__['file.remove'](cfg_file)

    return ret


def info(zone, show_all=False):
    '''
    Display the configuration from memory

    zone : string
        name of zone
    show_all : boolean
        also include calculated values like capped-cpu, cpu-shares, ...

    CLI Example:

    .. code-block:: bash

        salt '*' zonecfg.info tallgeese
    '''
    ret = {}

    # dump zone
    res = __salt__['cmd.run_all']('zonecfg -z {zone} info'.format(
        zone=zone,
    ))
    if res['retcode'] == 0:
        # parse output
        resname = None
        resdata = {}
        for line in res['stdout'].split("\n"):
            # skip some bad data
            if ':' not in line:
                continue

            # skip calculated values (if requested)
            if line.startswith('['):
                if not show_all:
                    continue
                line = line.rstrip()[1:-1]

            # extract key
            key = line.strip().split(':')[0]
            if '[' in key:
                key = key[1:]

            # parse calculated resource (if requested)
            if key in _zonecfg_info_resources_calculated:
                if resname:
                    ret[resname].append(resdata)
                if show_all:
                    resname = key
                    resdata = {}
                    if key not in ret:
                        ret[key] = []
                else:
                    resname = None
                    resdata = {}
            # parse resources
            elif key in _zonecfg_info_resources:
                if resname:
                    ret[resname].append(resdata)
                resname = key
                resdata = {}
                if key not in ret:
                    ret[key] = []
            # store resource property
            elif line.startswith("\t"):
                # ship calculated values (if requested)
                if line.strip().startswith('['):
                    if not show_all:
                        continue
                    line = line.strip()[1:-1]
                if key == 'property':  # handle special 'property' keys
                    if 'property' not in resdata:
                        resdata[key] = {}
                    kv = _parse_value(line.strip()[line.strip().index(':')+1:])
                    if 'name' in kv and 'value' in kv:
                        resdata[key][kv['name']] = kv['value']
                    else:
                        log.warning('zonecfg.info - not sure how to deal with: %s', kv)
                else:
                    resdata[key] = _parse_value(line.strip()[line.strip().index(':')+1:])
            # store property
            else:
                if resname:
                    ret[resname].append(resdata)
                resname = None
                resdata = {}
                if key == 'property':  # handle special 'property' keys
                    if 'property' not in ret:
                        ret[key] = {}
                    kv = _parse_value(line.strip()[line.strip().index(':')+1:])
                    if 'name' in kv and 'value' in kv:
                        res[key][kv['name']] = kv['value']
                    else:
                        log.warning('zonecfg.info - not sure how to deal with: %s', kv)
                else:
                    ret[key] = _parse_value(line.strip()[line.strip().index(':')+1:])
        # store hanging resource
        if resname:
            ret[resname].append(resdata)

    return ret

# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4