salt/modules/zonecfg.py
# -*- 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