salt/states/smartos.py
# -*- coding: utf-8 -*-
'''
Management of SmartOS Standalone Compute Nodes
:maintainer: Jorge Schrauwen <sjorge@blackdot.be>
:maturity: new
:depends: vmadm, imgadm
:platform: smartos
.. versionadded:: 2016.3.0
.. code-block:: yaml
vmtest.example.org:
smartos.vm_present:
- config:
reprovision: true
- vmconfig:
image_uuid: c02a2044-c1bd-11e4-bd8c-dfc1db8b0182
brand: joyent
alias: vmtest
quota: 5
max_physical_memory: 512
tags:
label: 'test vm'
owner: 'sjorge'
nics:
"82:1b:8e:49:e9:12":
nic_tag: trunk
mtu: 1500
ips:
- 172.16.1.123/16
- 192.168.2.123/24
vlan_id: 10
"82:1b:8e:49:e9:13":
nic_tag: trunk
mtu: 1500
ips:
- dhcp
vlan_id: 30
filesystems:
"/bigdata":
source: "/bulk/data"
type: lofs
options:
- ro
- nodevices
kvmtest.example.org:
smartos.vm_present:
- vmconfig:
brand: kvm
alias: kvmtest
cpu_type: host
ram: 512
vnc_port: 9
tags:
label: 'test kvm'
owner: 'sjorge'
disks:
disk0
size: 2048
model: virtio
compression: lz4
boot: true
nics:
"82:1b:8e:49:e9:15":
nic_tag: trunk
mtu: 1500
ips:
- dhcp
vlan_id: 30
docker.example.org:
smartos.vm_present:
- config:
auto_import: true
reprovision: true
- vmconfig:
image_uuid: emby/embyserver:latest
brand: lx
alias: mydockervm
quota: 5
max_physical_memory: 1024
tags:
label: 'my emby docker'
owner: 'sjorge'
resolvers:
- 172.16.1.1
nics:
"82:1b:8e:49:e9:18":
nic_tag: trunk
mtu: 1500
ips:
- 172.16.1.118/24
vlan_id: 10
filesystems:
"/config:
source: "/vmdata/emby_config"
type: lofs
options:
- nodevices
cleanup_images:
smartos.image_vacuum
.. note::
Keep in mind that when removing properties from vmconfig they will not get
removed from the vm's current configuration, except for nics, disk, tags, ...
they get removed via add_*, set_*, update_*, and remove_*. Properties must
be manually reset to their default value.
The same behavior as when using 'vmadm update'.
'''
from __future__ import absolute_import, unicode_literals, print_function
# Import Python libs
import logging
import json
import os
# Import Salt libs
import salt.utils.atomicfile
import salt.utils.data
import salt.utils.files
# Import 3rd party libs
from salt.ext import six
log = logging.getLogger(__name__)
# Define the state's virtual name
__virtualname__ = 'smartos'
def __virtual__():
'''
Provides smartos state provided for SmartOS
'''
if 'vmadm.create' in __salt__ and 'imgadm.list' in __salt__:
return True
else:
return (
False,
'{0} state module can only be loaded on SmartOS compute nodes'.format(
__virtualname__
)
)
def _split_docker_uuid(uuid):
'''
Split a smartos docker uuid into repo and tag
'''
if uuid:
uuid = uuid.split(':')
if len(uuid) == 2:
tag = uuid[1]
repo = uuid[0]
return repo, tag
return None, None
def _is_uuid(uuid):
'''
Check if uuid is a valid smartos uuid
Example: e69a0918-055d-11e5-8912-e3ceb6df4cf8
'''
if uuid and list((len(x) for x in uuid.split('-'))) == [8, 4, 4, 4, 12]:
return True
return False
def _is_docker_uuid(uuid):
'''
Check if uuid is a valid smartos docker uuid
Example plexinc/pms-docker:plexpass
'''
repo, tag = _split_docker_uuid(uuid)
return not (not repo and not tag)
def _load_config():
'''
Loads and parses /usbkey/config
'''
config = {}
if os.path.isfile('/usbkey/config'):
with salt.utils.files.fopen('/usbkey/config', 'r') as config_file:
for optval in config_file:
optval = salt.utils.stringutils.to_unicode(optval)
if optval[0] == '#':
continue
if '=' not in optval:
continue
optval = optval.split('=')
config[optval[0].lower()] = optval[1].strip().strip('"')
log.debug('smartos.config - read /usbkey/config: %s', config)
return config
def _write_config(config):
'''
writes /usbkey/config
'''
try:
with salt.utils.atomicfile.atomic_open('/usbkey/config', 'w') as config_file:
config_file.write("#\n# This file was generated by salt\n#\n")
for prop in salt.utils.odict.OrderedDict(sorted(config.items())):
if ' ' in six.text_type(config[prop]):
if not config[prop].startswith('"') or not config[prop].endswith('"'):
config[prop] = '"{0}"'.format(config[prop])
config_file.write(
salt.utils.stringutils.to_str(
"{0}={1}\n".format(prop, config[prop])
)
)
log.debug('smartos.config - wrote /usbkey/config: %s', config)
except IOError:
return False
return True
def _parse_vmconfig(config, instances):
'''
Parse vm_present vm config
'''
vmconfig = None
if isinstance(config, (salt.utils.odict.OrderedDict)):
vmconfig = salt.utils.odict.OrderedDict()
for prop in config:
if prop not in instances:
vmconfig[prop] = config[prop]
else:
if not isinstance(config[prop], (salt.utils.odict.OrderedDict)):
continue
vmconfig[prop] = []
for instance in config[prop]:
instance_config = config[prop][instance]
instance_config[instances[prop]] = instance
## some property are lowercase
if 'mac' in instance_config:
instance_config['mac'] = instance_config['mac'].lower()
vmconfig[prop].append(instance_config)
else:
log.error('smartos.vm_present::parse_vmconfig - failed to parse')
return vmconfig
def _get_instance_changes(current, state):
'''
get modified properties
'''
# get keys
current_keys = set(current.keys())
state_keys = set(state.keys())
# compare configs
changed = salt.utils.data.compare_dicts(current, state)
for change in salt.utils.data.compare_dicts(current, state):
if change in changed and changed[change]['old'] == "":
del changed[change]
if change in changed and changed[change]['new'] == "":
del changed[change]
return changed
def _copy_lx_vars(vmconfig):
# NOTE: documentation on dockerinit: https://github.com/joyent/smartos-live/blob/master/src/dockerinit/README.md
if 'image_uuid' in vmconfig:
# NOTE: retrieve tags and type from image
imgconfig = __salt__['imgadm.get'](vmconfig['image_uuid']).get('manifest', {})
imgtype = imgconfig.get('type', 'zone-dataset')
imgtags = imgconfig.get('tags', {})
# NOTE: copy kernel_version (if not specified in vmconfig)
if 'kernel_version' not in vmconfig and 'kernel_version' in imgtags:
vmconfig['kernel_version'] = imgtags['kernel_version']
# NOTE: copy docker vars
if imgtype == 'docker':
vmconfig['docker'] = True
vmconfig['kernel_version'] = vmconfig.get('kernel_version', '4.3.0')
if 'internal_metadata' not in vmconfig:
vmconfig['internal_metadata'] = {}
for var in imgtags.get('docker:config', {}):
val = imgtags['docker:config'][var]
var = 'docker:{0}'.format(var.lower())
# NOTE: skip empty values
if not val:
continue
# NOTE: skip or merge user values
if var == 'docker:env':
try:
val_config = json.loads(
vmconfig['internal_metadata'].get(var, "")
)
except ValueError as e:
val_config = []
for config_env_var in val_config if isinstance(val_config, list) else json.loads(val_config):
config_env_var = config_env_var.split('=')
for img_env_var in val:
if img_env_var.startswith('{0}='.format(config_env_var[0])):
val.remove(img_env_var)
val.append('='.join(config_env_var))
elif var in vmconfig['internal_metadata']:
continue
if isinstance(val, list):
# NOTE: string-encoded JSON arrays
vmconfig['internal_metadata'][var] = json.dumps(val)
else:
vmconfig['internal_metadata'][var] = val
return vmconfig
def config_present(name, value):
'''
Ensure configuration property is set to value in /usbkey/config
name : string
name of property
value : string
value of property
'''
name = name.lower()
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
# load confiration
config = _load_config()
# handle bool and None value
if isinstance(value, (bool)):
value = 'true' if value else 'false'
if not value:
value = ""
if name in config:
if six.text_type(config[name]) == six.text_type(value):
# we're good
ret['result'] = True
ret['comment'] = 'property {0} already has value "{1}"'.format(name, value)
else:
# update property
ret['result'] = True
ret['comment'] = 'updated property {0} with value "{1}"'.format(name, value)
ret['changes'][name] = value
config[name] = value
else:
# add property
ret['result'] = True
ret['comment'] = 'added property {0} with value "{1}"'.format(name, value)
ret['changes'][name] = value
config[name] = value
# apply change if needed
if not __opts__['test'] and ret['changes']:
ret['result'] = _write_config(config)
return ret
def config_absent(name):
'''
Ensure configuration property is absent in /usbkey/config
name : string
name of property
'''
name = name.lower()
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
# load configuration
config = _load_config()
if name in config:
# delete property
ret['result'] = True
ret['comment'] = 'property {0} deleted'.format(name)
ret['changes'][name] = None
del config[name]
else:
# we're good
ret['result'] = True
ret['comment'] = 'property {0} is absent'.format(name)
# apply change if needed
if not __opts__['test'] and ret['changes']:
ret['result'] = _write_config(config)
return ret
def source_present(name, source_type='imgapi'):
'''
Ensure an image source is present on the computenode
name : string
source url
source_type : string
source type (imgapi or docker)
'''
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
if name in __salt__['imgadm.sources']():
# source is present
ret['result'] = True
ret['comment'] = 'image source {0} is present'.format(name)
else:
# add new source
if __opts__['test']:
res = {}
ret['result'] = True
else:
res = __salt__['imgadm.source_add'](name, source_type)
ret['result'] = (name in res)
if ret['result']:
ret['comment'] = 'image source {0} added'.format(name)
ret['changes'][name] = 'added'
else:
ret['comment'] = 'image source {0} not added'.format(name)
if 'Error' in res:
ret['comment'] = '{0}: {1}'.format(ret['comment'], res['Error'])
return ret
def source_absent(name):
'''
Ensure an image source is absent on the computenode
name : string
source url
'''
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
if name not in __salt__['imgadm.sources']():
# source is absent
ret['result'] = True
ret['comment'] = 'image source {0} is absent'.format(name)
else:
# remove source
if __opts__['test']:
res = {}
ret['result'] = True
else:
res = __salt__['imgadm.source_delete'](name)
ret['result'] = (name not in res)
if ret['result']:
ret['comment'] = 'image source {0} deleted'.format(name)
ret['changes'][name] = 'deleted'
else:
ret['comment'] = 'image source {0} not deleted'.format(name)
if 'Error' in res:
ret['comment'] = '{0}: {1}'.format(ret['comment'], res['Error'])
return ret
def image_present(name):
'''
Ensure image is present on the computenode
name : string
uuid of image
'''
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
if _is_docker_uuid(name) and __salt__['imgadm.docker_to_uuid'](name):
# docker image was imported
ret['result'] = True
ret['comment'] = 'image {0} ({1}) is present'.format(
name,
__salt__['imgadm.docker_to_uuid'](name),
)
elif name in __salt__['imgadm.list']():
# image was already imported
ret['result'] = True
ret['comment'] = 'image {0} is present'.format(name)
else:
# add image
if _is_docker_uuid(name):
# NOTE: we cannot query available docker images
available_images = [name]
else:
available_images = __salt__['imgadm.avail']()
if name in available_images:
if __opts__['test']:
ret['result'] = True
res = {}
if _is_docker_uuid(name):
res['00000000-0000-0000-0000-000000000000'] = name
else:
res[name] = available_images[name]
else:
res = __salt__['imgadm.import'](name)
if _is_uuid(name):
ret['result'] = (name in res)
elif _is_docker_uuid(name):
ret['result'] = __salt__['imgadm.docker_to_uuid'](name) is not None
if ret['result']:
ret['comment'] = 'image {0} imported'.format(name)
ret['changes'] = res
else:
ret['comment'] = 'image {0} was unable to be imported'.format(name)
else:
ret['result'] = False
ret['comment'] = 'image {0} does not exists'.format(name)
return ret
def image_absent(name):
'''
Ensure image is absent on the computenode
name : string
uuid of image
.. note::
computenode.image_absent will only remove the image if it is not used
by a vm.
'''
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
uuid = None
if _is_uuid(name):
uuid = name
if _is_docker_uuid(name):
uuid = __salt__['imgadm.docker_to_uuid'](name)
if not uuid or uuid not in __salt__['imgadm.list']():
# image not imported
ret['result'] = True
ret['comment'] = 'image {0} is absent'.format(name)
else:
# check if image in use by vm
if uuid in __salt__['vmadm.list'](order='image_uuid'):
ret['result'] = False
ret['comment'] = 'image {0} currently in use by a vm'.format(name)
else:
# delete image
if __opts__['test']:
ret['result'] = True
else:
image = __salt__['imgadm.get'](uuid)
image_count = 0
if image['manifest']['name'] == 'docker-layer':
# NOTE: docker images are made of multiple layers, loop over them
while image:
image_count += 1
__salt__['imgadm.delete'](image['manifest']['uuid'])
if 'origin' in image['manifest']:
image = __salt__['imgadm.get'](image['manifest']['origin'])
else:
image = None
else:
# NOTE: normal images can just be delete
__salt__['imgadm.delete'](uuid)
ret['result'] = uuid not in __salt__['imgadm.list']()
if image_count:
ret['comment'] = 'image {0} and {1} children deleted'.format(name, image_count)
else:
ret['comment'] = 'image {0} deleted'.format(name)
ret['changes'][name] = None
return ret
def image_vacuum(name):
'''
Delete images not in use or installed via image_present
.. warning::
Only image_present states that are included via the
top file will be detected.
'''
name = name.lower()
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
# list of images to keep
images = []
# retrieve image_present state data for host
for state in __salt__['state.show_lowstate']():
# don't throw exceptions when not highstate run
if 'state' not in state:
continue
# skip if not from this state module
if state['state'] != __virtualname__:
continue
# skip if not image_present
if state['fun'] not in ['image_present']:
continue
# keep images installed via image_present
if 'name' in state:
if _is_uuid(state['name']):
images.append(state['name'])
elif _is_docker_uuid(state['name']):
state['name'] = __salt__['imgadm.docker_to_uuid'](state['name'])
if not state['name']:
continue
images.append(state['name'])
# retrieve images in use by vms
for image_uuid in __salt__['vmadm.list'](order='image_uuid'):
if image_uuid not in images:
images.append(image_uuid)
# purge unused images
ret['result'] = True
for image_uuid in __salt__['imgadm.list']():
if image_uuid in images:
continue
image = __salt__['imgadm.get'](image_uuid)
if image['manifest']['name'] == 'docker-layer':
# NOTE: docker images are made of multiple layers, loop over them
while image:
image_uuid = image['manifest']['uuid']
if image_uuid in __salt__['imgadm.delete'](image_uuid):
ret['changes'][image_uuid] = None
else:
ret['result'] = False
ret['comment'] = 'failed to delete images'
if 'origin' in image['manifest']:
image = __salt__['imgadm.get'](image['manifest']['origin'])
else:
image = None
else:
# NOTE: normal images can just be delete
if image_uuid in __salt__['imgadm.delete'](image_uuid):
ret['changes'][image_uuid] = None
else:
ret['result'] = False
ret['comment'] = 'failed to delete images'
if ret['result'] and not ret['changes']:
ret['comment'] = 'no images deleted'
elif ret['result'] and ret['changes']:
ret['comment'] = 'images deleted'
return ret
def vm_present(name, vmconfig, config=None):
'''
Ensure vm is present on the computenode
name : string
hostname of vm
vmconfig : dict
options to set for the vm
config : dict
fine grain control over vm_present
.. note::
The following configuration properties can be toggled in the config parameter.
- kvm_reboot (true) - reboots of kvm zones if needed for a config update
- auto_import (false) - automatic importing of missing images
- auto_lx_vars (true) - copy kernel_version and docker:* variables from image
- reprovision (false) - reprovision on image_uuid changes
- enforce_tags (true) - false = add tags only, true = add, update, and remove tags
- enforce_routes (true) - false = add tags only, true = add, update, and remove routes
- enforce_internal_metadata (true) - false = add metadata only, true = add, update, and remove metadata
- enforce_customer_metadata (true) - false = add metadata only, true = add, update, and remove metadata
.. note::
State ID is used as hostname. Hostnames must be unique.
.. note::
If hostname is provided in vmconfig this will take president over the State ID.
This allows multiple states to be applied to the same vm.
.. note::
The following instances should have a unique ID.
- nic : mac
- filesystem: target
- disk : path or diskN for zvols
e.g. disk0 will be the first disk added, disk1 the 2nd,...
.. versionchanged:: 2019.2.0
Added support for docker image uuids, added auto_lx_vars configuration, documented some missing configuration options.
'''
name = name.lower()
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
# config defaults
state_config = config if config else {}
config = {
'kvm_reboot': True,
'auto_import': False,
'auto_lx_vars': True,
'reprovision': False,
'enforce_tags': True,
'enforce_routes': True,
'enforce_internal_metadata': True,
'enforce_customer_metadata': True,
}
config.update(state_config)
log.debug('smartos.vm_present::%s::config - %s', name, config)
# map special vmconfig parameters
# collections have set/remove handlers
# instances have add/update/remove handlers and a unique id
vmconfig_type = {
'collection': [
'tags',
'customer_metadata',
'internal_metadata',
'routes'
],
'instance': {
'nics': 'mac',
'disks': 'path',
'filesystems': 'target'
},
'create_only': [
'filesystems'
]
}
vmconfig_docker_keep = [
'docker:id',
'docker:restartcount',
]
vmconfig_docker_array = [
'docker:env',
'docker:cmd',
'docker:entrypoint',
]
# parse vmconfig
vmconfig = _parse_vmconfig(vmconfig, vmconfig_type['instance'])
log.debug('smartos.vm_present::%s::vmconfig - %s', name, vmconfig)
# set hostname if needed
if 'hostname' not in vmconfig:
vmconfig['hostname'] = name
# prepare image_uuid
if 'image_uuid' in vmconfig:
# NOTE: lookup uuid from docker uuid (normal uuid's are passed throuhg unmodified)
# we must do this again if we end up importing a missing image later!
docker_uuid = __salt__['imgadm.docker_to_uuid'](vmconfig['image_uuid'])
vmconfig['image_uuid'] = docker_uuid if docker_uuid else vmconfig['image_uuid']
# NOTE: import image (if missing and allowed)
if vmconfig['image_uuid'] not in __salt__['imgadm.list']():
if config['auto_import']:
if not __opts__['test']:
res = __salt__['imgadm.import'](vmconfig['image_uuid'])
vmconfig['image_uuid'] = __salt__['imgadm.docker_to_uuid'](vmconfig['image_uuid'])
if vmconfig['image_uuid'] not in res:
ret['result'] = False
ret['comment'] = 'failed to import image {0}'.format(vmconfig['image_uuid'])
else:
ret['result'] = False
ret['comment'] = 'image {0} not installed'.format(vmconfig['image_uuid'])
# prepare disk.*.image_uuid
for disk in vmconfig['disks'] if 'disks' in vmconfig else []:
if 'image_uuid' in disk and disk['image_uuid'] not in __salt__['imgadm.list']():
if config['auto_import']:
if not __opts__['test']:
res = __salt__['imgadm.import'](disk['image_uuid'])
if disk['image_uuid'] not in res:
ret['result'] = False
ret['comment'] = 'failed to import image {0}'.format(disk['image_uuid'])
else:
ret['result'] = False
ret['comment'] = 'image {0} not installed'.format(disk['image_uuid'])
# docker json-array handling
if 'internal_metadata' in vmconfig:
for var in vmconfig_docker_array:
if var not in vmconfig['internal_metadata']:
continue
if isinstance(vmconfig['internal_metadata'][var], list):
vmconfig['internal_metadata'][var] = json.dumps(
vmconfig['internal_metadata'][var]
)
# copy lx variables
if vmconfig['brand'] == 'lx' and config['auto_lx_vars']:
# NOTE: we can only copy the lx vars after the image has bene imported
vmconfig = _copy_lx_vars(vmconfig)
# quick abort if things look wrong
# NOTE: use explicit check for false, otherwise None also matches!
if ret['result'] is False:
return ret
# check if vm exists
if vmconfig['hostname'] in __salt__['vmadm.list'](order='hostname'):
# update vm
ret['result'] = True
# expand vmconfig
vmconfig = {
'state': vmconfig,
'current': __salt__['vmadm.get'](vmconfig['hostname'], key='hostname'),
'changed': {},
'reprovision_uuid': None
}
# prepare reprovision
if 'image_uuid' in vmconfig['state']:
vmconfig['reprovision_uuid'] = vmconfig['state']['image_uuid']
vmconfig['state']['image_uuid'] = vmconfig['current']['image_uuid']
# disks need some special care
if 'disks' in vmconfig['state']:
new_disks = []
for disk in vmconfig['state']['disks']:
path = False
if 'disks' in vmconfig['current']:
for cdisk in vmconfig['current']['disks']:
if cdisk['path'].endswith(disk['path']):
path = cdisk['path']
break
if not path:
del disk['path']
else:
disk['path'] = path
new_disks.append(disk)
vmconfig['state']['disks'] = new_disks
# process properties
for prop in vmconfig['state']:
# skip special vmconfig_types
if prop in vmconfig_type['instance'] or \
prop in vmconfig_type['collection'] or \
prop in vmconfig_type['create_only']:
continue
# skip unchanged properties
if prop in vmconfig['current']:
if isinstance(vmconfig['current'][prop], (list)) or isinstance(vmconfig['current'][prop], (dict)):
if vmconfig['current'][prop] == vmconfig['state'][prop]:
continue
else:
if "{0}".format(vmconfig['current'][prop]) == "{0}".format(vmconfig['state'][prop]):
continue
# add property to changeset
vmconfig['changed'][prop] = vmconfig['state'][prop]
# process collections
for collection in vmconfig_type['collection']:
# skip create only collections
if collection in vmconfig_type['create_only']:
continue
# enforcement
enforce = config['enforce_{0}'.format(collection)]
log.debug('smartos.vm_present::enforce_%s = %s', collection, enforce)
# dockerinit handling
if collection == 'internal_metadata' and vmconfig['state'].get('docker', False):
if 'internal_metadata' not in vmconfig['state']:
vmconfig['state']['internal_metadata'] = {}
# preserve some docker specific metadata (added and needed by dockerinit)
for var in vmconfig_docker_keep:
val = vmconfig['current'].get(collection, {}).get(var, None)
if val is not None:
vmconfig['state']['internal_metadata'][var] = val
# process add and update for collection
if collection in vmconfig['state'] and vmconfig['state'][collection] is not None:
for prop in vmconfig['state'][collection]:
# skip unchanged properties
if prop in vmconfig['current'][collection] and \
vmconfig['current'][collection][prop] == vmconfig['state'][collection][prop]:
continue
# skip update if not enforcing
if not enforce and prop in vmconfig['current'][collection]:
continue
# create set_ dict
if 'set_{0}'.format(collection) not in vmconfig['changed']:
vmconfig['changed']['set_{0}'.format(collection)] = {}
# add property to changeset
vmconfig['changed']['set_{0}'.format(collection)][prop] = vmconfig['state'][collection][prop]
# process remove for collection
if enforce and collection in vmconfig['current'] and vmconfig['current'][collection] is not None:
for prop in vmconfig['current'][collection]:
# skip if exists in state
if collection in vmconfig['state'] and vmconfig['state'][collection] is not None:
if prop in vmconfig['state'][collection]:
continue
# create remove_ array
if 'remove_{0}'.format(collection) not in vmconfig['changed']:
vmconfig['changed']['remove_{0}'.format(collection)] = []
# remove property
vmconfig['changed']['remove_{0}'.format(collection)].append(prop)
# process instances
for instance in vmconfig_type['instance']:
# skip create only instances
if instance in vmconfig_type['create_only']:
continue
# add or update instances
if instance in vmconfig['state'] and vmconfig['state'][instance] is not None:
for state_cfg in vmconfig['state'][instance]:
add_instance = True
# find instance with matching ids
for current_cfg in vmconfig['current'][instance]:
if vmconfig_type['instance'][instance] not in state_cfg:
continue
if state_cfg[vmconfig_type['instance'][instance]] == current_cfg[vmconfig_type['instance'][instance]]:
# ids have matched, disable add instance
add_instance = False
changed = _get_instance_changes(current_cfg, state_cfg)
update_cfg = {}
# handle changes
for prop in changed:
update_cfg[prop] = state_cfg[prop]
# handle new properties
for prop in state_cfg:
# skip empty props like ips, options,..
if isinstance(state_cfg[prop], (list)) and not state_cfg[prop]:
continue
if prop not in current_cfg:
update_cfg[prop] = state_cfg[prop]
# update instance
if update_cfg:
# create update_ array
if 'update_{0}'.format(instance) not in vmconfig['changed']:
vmconfig['changed']['update_{0}'.format(instance)] = []
update_cfg[vmconfig_type['instance'][instance]] = state_cfg[vmconfig_type['instance'][instance]]
vmconfig['changed']['update_{0}'.format(instance)].append(update_cfg)
if add_instance:
# create add_ array
if 'add_{0}'.format(instance) not in vmconfig['changed']:
vmconfig['changed']['add_{0}'.format(instance)] = []
# add instance
vmconfig['changed']['add_{0}'.format(instance)].append(state_cfg)
# remove instances
if instance in vmconfig['current'] and vmconfig['current'][instance] is not None:
for current_cfg in vmconfig['current'][instance]:
remove_instance = True
# find instance with matching ids
if instance in vmconfig['state'] and vmconfig['state'][instance] is not None:
for state_cfg in vmconfig['state'][instance]:
if vmconfig_type['instance'][instance] not in state_cfg:
continue
if state_cfg[vmconfig_type['instance'][instance]] == current_cfg[vmconfig_type['instance'][instance]]:
# keep instance if matched
remove_instance = False
if remove_instance:
# create remove_ array
if 'remove_{0}'.format(instance) not in vmconfig['changed']:
vmconfig['changed']['remove_{0}'.format(instance)] = []
# remove instance
vmconfig['changed']['remove_{0}'.format(instance)].append(
current_cfg[vmconfig_type['instance'][instance]]
)
# update vm if we have pending changes
kvm_needs_start = False
if not __opts__['test'] and vmconfig['changed']:
# stop kvm if disk updates and kvm_reboot
if vmconfig['current']['brand'] == 'kvm' and config['kvm_reboot']:
if 'add_disks' in vmconfig['changed'] or \
'update_disks' in vmconfig['changed'] or \
'remove_disks' in vmconfig['changed']:
if vmconfig['state']['hostname'] in __salt__['vmadm.list'](order='hostname', search='state=running'):
kvm_needs_start = True
__salt__['vmadm.stop'](vm=vmconfig['state']['hostname'], key='hostname')
# do update
rret = __salt__['vmadm.update'](vm=vmconfig['state']['hostname'], key='hostname', **vmconfig['changed'])
if not isinstance(rret, (bool)) and 'Error' in rret:
ret['result'] = False
ret['comment'] = "{0}".format(rret['Error'])
else:
ret['result'] = True
ret['changes'][vmconfig['state']['hostname']] = vmconfig['changed']
if ret['result']:
if __opts__['test']:
ret['changes'][vmconfig['state']['hostname']] = vmconfig['changed']
if vmconfig['state']['hostname'] in ret['changes'] and ret['changes'][vmconfig['state']['hostname']]:
ret['comment'] = 'vm {0} updated'.format(vmconfig['state']['hostname'])
if config['kvm_reboot'] and vmconfig['current']['brand'] == 'kvm' and not __opts__['test']:
if vmconfig['state']['hostname'] in __salt__['vmadm.list'](order='hostname', search='state=running'):
__salt__['vmadm.reboot'](vm=vmconfig['state']['hostname'], key='hostname')
if kvm_needs_start:
__salt__['vmadm.start'](vm=vmconfig['state']['hostname'], key='hostname')
else:
ret['changes'] = {}
ret['comment'] = 'vm {0} is up to date'.format(vmconfig['state']['hostname'])
# reprovision (if required and allowed)
if 'image_uuid' in vmconfig['current'] and vmconfig['reprovision_uuid'] != vmconfig['current']['image_uuid']:
if config['reprovision']:
rret = __salt__['vmadm.reprovision'](
vm=vmconfig['state']['hostname'],
key='hostname',
image=vmconfig['reprovision_uuid']
)
if not isinstance(rret, (bool)) and 'Error' in rret:
ret['result'] = False
ret['comment'] = 'vm {0} updated, reprovision failed'.format(
vmconfig['state']['hostname']
)
else:
ret['comment'] = 'vm {0} updated and reprovisioned'.format(vmconfig['state']['hostname'])
if vmconfig['state']['hostname'] not in ret['changes']:
ret['changes'][vmconfig['state']['hostname']] = {}
ret['changes'][vmconfig['state']['hostname']]['image_uuid'] = vmconfig['reprovision_uuid']
else:
log.warning('smartos.vm_present::%s::reprovision - '
'image_uuid in state does not match current, '
'reprovision not allowed',
name)
else:
ret['comment'] = 'vm {0} failed to be updated'.format(vmconfig['state']['hostname'])
if not isinstance(rret, (bool)) and 'Error' in rret:
ret['comment'] = "{0}".format(rret['Error'])
else:
# check required image installed
ret['result'] = True
# disks need some special care
if 'disks' in vmconfig:
new_disks = []
for disk in vmconfig['disks']:
if 'path' in disk:
del disk['path']
new_disks.append(disk)
vmconfig['disks'] = new_disks
# create vm
if ret['result']:
uuid = __salt__['vmadm.create'](**vmconfig) if not __opts__['test'] else True
if not isinstance(uuid, (bool)) and 'Error' in uuid:
ret['result'] = False
ret['comment'] = "{0}".format(uuid['Error'])
else:
ret['result'] = True
ret['changes'][vmconfig['hostname']] = vmconfig
ret['comment'] = 'vm {0} created'.format(vmconfig['hostname'])
return ret
def vm_absent(name, archive=False):
'''
Ensure vm is absent on the computenode
name : string
hostname of vm
archive : boolean
toggle archiving of vm on removal
.. note::
State ID is used as hostname. Hostnames must be unique.
'''
name = name.lower()
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
if name not in __salt__['vmadm.list'](order='hostname'):
# we're good
ret['result'] = True
ret['comment'] = 'vm {0} is absent'.format(name)
else:
# delete vm
if not __opts__['test']:
# set archive to true if needed
if archive:
__salt__['vmadm.update'](vm=name, key='hostname', archive_on_delete=True)
ret['result'] = __salt__['vmadm.delete'](name, key='hostname')
else:
ret['result'] = True
if not isinstance(ret['result'], bool) and ret['result'].get('Error'):
ret['result'] = False
ret['comment'] = 'failed to delete vm {0}'.format(name)
else:
ret['comment'] = 'vm {0} deleted'.format(name)
ret['changes'][name] = None
return ret
def vm_running(name):
'''
Ensure vm is in the running state on the computenode
name : string
hostname of vm
.. note::
State ID is used as hostname. Hostnames must be unique.
'''
name = name.lower()
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
if name in __salt__['vmadm.list'](order='hostname', search='state=running'):
# we're good
ret['result'] = True
ret['comment'] = 'vm {0} already running'.format(name)
else:
# start the vm
ret['result'] = True if __opts__['test'] else __salt__['vmadm.start'](name, key='hostname')
if not isinstance(ret['result'], bool) and ret['result'].get('Error'):
ret['result'] = False
ret['comment'] = 'failed to start {0}'.format(name)
else:
ret['changes'][name] = 'running'
ret['comment'] = 'vm {0} started'.format(name)
return ret
def vm_stopped(name):
'''
Ensure vm is in the stopped state on the computenode
name : string
hostname of vm
.. note::
State ID is used as hostname. Hostnames must be unique.
'''
name = name.lower()
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
if name in __salt__['vmadm.list'](order='hostname', search='state=stopped'):
# we're good
ret['result'] = True
ret['comment'] = 'vm {0} already stopped'.format(name)
else:
# stop the vm
ret['result'] = True if __opts__['test'] else __salt__['vmadm.stop'](name, key='hostname')
if not isinstance(ret['result'], bool) and ret['result'].get('Error'):
ret['result'] = False
ret['comment'] = 'failed to stop {0}'.format(name)
else:
ret['changes'][name] = 'stopped'
ret['comment'] = 'vm {0} stopped'.format(name)
return ret
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4