Mini Shell
"""
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'.
.. warning::
For HVM (bhyve and KVM) brands the `image_uuid` field should go on the boot disks,
this disk should NOT have a size specified. (See man vmadm)
"""
import json
import logging
import os
import salt.utils.atomicfile
import salt.utils.data
import salt.utils.files
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,
"{} 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 str(config[prop]):
if not config[prop].startswith('"') or not config[prop].endswith(
'"'
):
config[prop] = f'"{config[prop]}"'
config_file.write(
salt.utils.stringutils.to_str(f"{prop}={config[prop]}\n")
)
log.debug("smartos.config - wrote /usbkey/config: %s", config)
except OSError:
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()
## calculate mac from vrrp_vrid
if "vrrp_vrid" in instance_config:
instance_config["mac"] = "00:00:5e:00:01:{}".format(
hex(int(instance_config["vrrp_vrid"]))
.split("x")[-1]
.zfill(2),
)
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 = f"docker:{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(f"{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 str(config[name]) == str(value):
# we're good
ret["result"] = True
ret["comment"] = f'property {name} already has value "{value}"'
else:
# update property
ret["result"] = True
ret["comment"] = f'updated property {name} with value "{value}"'
ret["changes"][name] = value
config[name] = value
else:
# add property
ret["result"] = True
ret["comment"] = f'added property {name} with value "{value}"'
ret["changes"][name] = value
config[name] = value
# apply change if needed
if not __opts__["test"] and ret["changes"]:
ret["result"] = _write_config(config)
if not ret["result"]:
ret["comment"] = (
'Could not add property {} with value "{}" to config'.format(
name, value
)
)
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"] = f"property {name} deleted"
ret["changes"][name] = None
del config[name]
else:
# we're good
ret["result"] = True
ret["comment"] = f"property {name} is absent"
# 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"] = f"image source {name} is present"
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"] = f"image source {name} added"
ret["changes"][name] = "added"
else:
ret["comment"] = f"image source {name} not added"
if "Error" in res:
ret["comment"] = "{}: {}".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"] = f"image source {name} is absent"
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"] = f"image source {name} deleted"
ret["changes"][name] = "deleted"
else:
ret["comment"] = f"image source {name} not deleted"
if "Error" in res:
ret["comment"] = "{}: {}".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 {} ({}) 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"] = f"image {name} is present"
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"] = f"image {name} imported"
ret["changes"] = res
else:
ret["comment"] = f"image {name} was unable to be imported"
else:
ret["result"] = False
ret["comment"] = f"image {name} does not exists"
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"] = f"image {name} is absent"
else:
# check if image in use by vm
if uuid in __salt__["vmadm.list"](order="image_uuid"):
ret["result"] = False
ret["comment"] = f"image {name} currently in use by a vm"
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 {} and {} children deleted".format(
name, image_count
)
else:
ret["comment"] = f"image {name} deleted"
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",
"pci_devices": "path",
},
"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 {}".format(
vmconfig["image_uuid"]
)
else:
ret["result"] = False
ret["comment"] = "image {} 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 {}".format(
disk["image_uuid"]
)
else:
ret["result"] = False
ret["comment"] = "image {} 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 "{}".format(vmconfig["current"][prop]) == "{}".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[f"enforce_{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 f"set_{collection}" not in vmconfig["changed"]:
vmconfig["changed"][f"set_{collection}"] = {}
# add property to changeset
vmconfig["changed"][f"set_{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 f"remove_{collection}" not in vmconfig["changed"]:
vmconfig["changed"][f"remove_{collection}"] = []
# remove property
vmconfig["changed"][f"remove_{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 f"update_{instance}" not in vmconfig["changed"]:
vmconfig["changed"][f"update_{instance}"] = []
update_cfg[vmconfig_type["instance"][instance]] = (
state_cfg[vmconfig_type["instance"][instance]]
)
vmconfig["changed"][f"update_{instance}"].append(
update_cfg
)
if add_instance:
# create add_ array
if f"add_{instance}" not in vmconfig["changed"]:
vmconfig["changed"][f"add_{instance}"] = []
# add instance
vmconfig["changed"][f"add_{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 f"remove_{instance}" not in vmconfig["changed"]:
vmconfig["changed"][f"remove_{instance}"] = []
# remove instance
vmconfig["changed"][f"remove_{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"] = "{}".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 {} 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 {} 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 {} updated, reprovision failed".format(
vmconfig["state"]["hostname"]
)
else:
ret["comment"] = "vm {} 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 {} failed to be updated".format(
vmconfig["state"]["hostname"]
)
if not isinstance(rret, (bool)) and "Error" in rret:
ret["comment"] = "{}".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"] = "{}".format(uuid["Error"])
else:
ret["result"] = True
ret["changes"][vmconfig["hostname"]] = vmconfig
ret["comment"] = "vm {} 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"] = f"vm {name} is absent"
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"] = f"failed to delete vm {name}"
else:
ret["comment"] = f"vm {name} deleted"
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"] = f"vm {name} already running"
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"] = f"failed to start {name}"
else:
ret["changes"][name] = "running"
ret["comment"] = f"vm {name} started"
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"] = f"vm {name} already stopped"
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"] = f"failed to stop {name}"
else:
ret["changes"][name] = "stopped"
ret["comment"] = f"vm {name} stopped"
return ret
Zerion Mini Shell 1.0