golden hour
/opt/saltstack/salt/lib/python3.10/site-packages/salt/states
⬆️ Go Up
Upload
File/Folder
Size
Actions
__init__.py
25 B
Del
OK
__pycache__
-
Del
OK
acme.py
5.08 KB
Del
OK
alias.py
2.49 KB
Del
OK
alternatives.py
6.75 KB
Del
OK
ansiblegate.py
7.93 KB
Del
OK
apache.py
3.95 KB
Del
OK
apache_conf.py
2.72 KB
Del
OK
apache_module.py
2.73 KB
Del
OK
apache_site.py
2.66 KB
Del
OK
aptpkg.py
1.42 KB
Del
OK
archive.py
68.24 KB
Del
OK
artifactory.py
6.84 KB
Del
OK
at.py
7.48 KB
Del
OK
augeas.py
10.57 KB
Del
OK
aws_sqs.py
2.59 KB
Del
OK
azurearm_compute.py
11.78 KB
Del
OK
azurearm_dns.py
26.05 KB
Del
OK
azurearm_network.py
89.12 KB
Del
OK
azurearm_resource.py
28.23 KB
Del
OK
beacon.py
7.58 KB
Del
OK
bigip.py
96.63 KB
Del
OK
blockdev.py
5.13 KB
Del
OK
boto3_elasticache.py
48.01 KB
Del
OK
boto3_elasticsearch.py
32.58 KB
Del
OK
boto3_route53.py
37.54 KB
Del
OK
boto3_sns.py
12.69 KB
Del
OK
boto_apigateway.py
82.83 KB
Del
OK
boto_asg.py
31.93 KB
Del
OK
boto_cfn.py
11.53 KB
Del
OK
boto_cloudfront.py
6.01 KB
Del
OK
boto_cloudtrail.py
13.18 KB
Del
OK
boto_cloudwatch_alarm.py
6.4 KB
Del
OK
boto_cloudwatch_event.py
12.33 KB
Del
OK
boto_cognitoidentity.py
13.69 KB
Del
OK
boto_datapipeline.py
18.5 KB
Del
OK
boto_dynamodb.py
29.32 KB
Del
OK
boto_ec2.py
71.98 KB
Del
OK
boto_elasticache.py
16.75 KB
Del
OK
boto_elasticsearch_domain.py
12.27 KB
Del
OK
boto_elb.py
55.1 KB
Del
OK
boto_elbv2.py
12.19 KB
Del
OK
boto_iam.py
69.16 KB
Del
OK
boto_iam_role.py
27.12 KB
Del
OK
boto_iot.py
25.33 KB
Del
OK
boto_kinesis.py
16.69 KB
Del
OK
boto_kms.py
12.11 KB
Del
OK
boto_lambda.py
35.52 KB
Del
OK
boto_lc.py
11.04 KB
Del
OK
boto_rds.py
26 KB
Del
OK
boto_route53.py
19.49 KB
Del
OK
boto_s3.py
9.32 KB
Del
OK
boto_s3_bucket.py
24.67 KB
Del
OK
boto_secgroup.py
32.62 KB
Del
OK
boto_sns.py
8.92 KB
Del
OK
boto_sqs.py
7.97 KB
Del
OK
boto_vpc.py
62.23 KB
Del
OK
bower.py
8.26 KB
Del
OK
btrfs.py
10.34 KB
Del
OK
cabal.py
5.73 KB
Del
OK
ceph.py
1.9 KB
Del
OK
chef.py
3.76 KB
Del
OK
chocolatey.py
16.15 KB
Del
OK
chronos_job.py
4.6 KB
Del
OK
cimc.py
14.32 KB
Del
OK
cisconso.py
3.14 KB
Del
OK
cloud.py
14.4 KB
Del
OK
cmd.py
40.92 KB
Del
OK
composer.py
8.38 KB
Del
OK
consul.py
5.4 KB
Del
OK
cron.py
23.39 KB
Del
OK
cryptdev.py
6.17 KB
Del
OK
csf.py
9.98 KB
Del
OK
cyg.py
7.05 KB
Del
OK
ddns.py
4.2 KB
Del
OK
debconfmod.py
6.33 KB
Del
OK
dellchassis.py
24.49 KB
Del
OK
disk.py
6.49 KB
Del
OK
docker_container.py
85.27 KB
Del
OK
docker_image.py
16.7 KB
Del
OK
docker_network.py
36.78 KB
Del
OK
docker_volume.py
6.72 KB
Del
OK
drac.py
4.17 KB
Del
OK
dvs.py
26.29 KB
Del
OK
elasticsearch.py
20.38 KB
Del
OK
elasticsearch_index.py
3.25 KB
Del
OK
elasticsearch_index_template.py
3.67 KB
Del
OK
environ.py
5.81 KB
Del
OK
eselect.py
2.27 KB
Del
OK
esxcluster.py
22.4 KB
Del
OK
esxdatacenter.py
4.44 KB
Del
OK
esxi.py
63.07 KB
Del
OK
esxvm.py
20.11 KB
Del
OK
etcd_mod.py
11 KB
Del
OK
ethtool.py
9.88 KB
Del
OK
event.py
2.48 KB
Del
OK
file.py
316.7 KB
Del
OK
firewall.py
1.33 KB
Del
OK
firewalld.py
26.08 KB
Del
OK
gem.py
7.13 KB
Del
OK
git.py
123.85 KB
Del
OK
github.py
27.25 KB
Del
OK
glance_image.py
2.26 KB
Del
OK
glassfish.py
21.47 KB
Del
OK
glusterfs.py
12.21 KB
Del
OK
gnomedesktop.py
7.47 KB
Del
OK
gpg.py
5.28 KB
Del
OK
grafana.py
12.11 KB
Del
OK
grafana4_dashboard.py
17.31 KB
Del
OK
grafana4_datasource.py
6.15 KB
Del
OK
grafana4_org.py
7.73 KB
Del
OK
grafana4_user.py
5.52 KB
Del
OK
grafana_dashboard.py
17.74 KB
Del
OK
grafana_datasource.py
5.31 KB
Del
OK
grains.py
15.57 KB
Del
OK
group.py
9.84 KB
Del
OK
heat.py
9.69 KB
Del
OK
helm.py
10.39 KB
Del
OK
hg.py
6.33 KB
Del
OK
highstate_doc.py
1.41 KB
Del
OK
host.py
8.64 KB
Del
OK
http.py
7.46 KB
Del
OK
icinga2.py
9.07 KB
Del
OK
idem.py
3.91 KB
Del
OK
ifttt.py
2.12 KB
Del
OK
incron.py
5.71 KB
Del
OK
influxdb08_database.py
2.85 KB
Del
OK
influxdb08_user.py
3.39 KB
Del
OK
influxdb_continuous_query.py
2.83 KB
Del
OK
influxdb_database.py
2.11 KB
Del
OK
influxdb_retention_policy.py
4.82 KB
Del
OK
influxdb_user.py
4.84 KB
Del
OK
infoblox_a.py
4.24 KB
Del
OK
infoblox_cname.py
4.19 KB
Del
OK
infoblox_host_record.py
6.59 KB
Del
OK
infoblox_range.py
6.85 KB
Del
OK
ini_manage.py
12.67 KB
Del
OK
ipmi.py
8.42 KB
Del
OK
ipset.py
9.66 KB
Del
OK
iptables.py
27.65 KB
Del
OK
jboss7.py
23.95 KB
Del
OK
jenkins.py
3.36 KB
Del
OK
junos.py
17.78 KB
Del
OK
kapacitor.py
6.46 KB
Del
OK
kernelpkg.py
6.42 KB
Del
OK
keyboard.py
2.01 KB
Del
OK
keystone.py
27.12 KB
Del
OK
keystone_domain.py
2.81 KB
Del
OK
keystone_endpoint.py
4.69 KB
Del
OK
keystone_group.py
3.25 KB
Del
OK
keystone_project.py
3.36 KB
Del
OK
keystone_role.py
2.33 KB
Del
OK
keystone_role_grant.py
4.08 KB
Del
OK
keystone_service.py
2.89 KB
Del
OK
keystone_user.py
3.47 KB
Del
OK
keystore.py
5.67 KB
Del
OK
kmod.py
8.59 KB
Del
OK
kubernetes.py
24.87 KB
Del
OK
layman.py
2.44 KB
Del
OK
ldap.py
19.78 KB
Del
OK
libcloud_dns.py
5.7 KB
Del
OK
libcloud_loadbalancer.py
5.66 KB
Del
OK
libcloud_storage.py
5.13 KB
Del
OK
linux_acl.py
24.42 KB
Del
OK
locale.py
2.52 KB
Del
OK
logadm.py
4.67 KB
Del
OK
logrotate.py
3.86 KB
Del
OK
loop.py
7.74 KB
Del
OK
lvm.py
13.33 KB
Del
OK
lvs_server.py
6.28 KB
Del
OK
lvs_service.py
4.38 KB
Del
OK
lxc.py
22.17 KB
Del
OK
lxd.py
7.88 KB
Del
OK
lxd_container.py
22.25 KB
Del
OK
lxd_image.py
10.59 KB
Del
OK
lxd_profile.py
7.11 KB
Del
OK
mac_assistive.py
1.55 KB
Del
OK
mac_keychain.py
5.59 KB
Del
OK
mac_xattr.py
3.15 KB
Del
OK
macdefaults.py
2.65 KB
Del
OK
macpackage.py
6.76 KB
Del
OK
makeconf.py
6.87 KB
Del
OK
marathon_app.py
4.45 KB
Del
OK
mdadm_raid.py
6.41 KB
Del
OK
memcached.py
3.95 KB
Del
OK
modjk.py
2.84 KB
Del
OK
modjk_worker.py
6.49 KB
Del
OK
module.py
18.64 KB
Del
OK
mongodb_database.py
1.65 KB
Del
OK
mongodb_user.py
6.26 KB
Del
OK
monit.py
2.68 KB
Del
OK
mount.py
50.32 KB
Del
OK
mssql_database.py
3 KB
Del
OK
mssql_login.py
3.64 KB
Del
OK
mssql_role.py
2.37 KB
Del
OK
mssql_user.py
3.51 KB
Del
OK
msteams.py
2.53 KB
Del
OK
mysql_database.py
6.05 KB
Del
OK
mysql_grants.py
8.49 KB
Del
OK
mysql_query.py
13.07 KB
Del
OK
mysql_user.py
9.51 KB
Del
OK
net_napalm_yang.py
9.15 KB
Del
OK
netacl.py
31.92 KB
Del
OK
netconfig.py
33.42 KB
Del
OK
netntp.py
12.51 KB
Del
OK
netsnmp.py
11.33 KB
Del
OK
netusers.py
16.1 KB
Del
OK
network.py
23.97 KB
Del
OK
neutron_network.py
3.96 KB
Del
OK
neutron_secgroup.py
4 KB
Del
OK
neutron_secgroup_rule.py
4.75 KB
Del
OK
neutron_subnet.py
4.29 KB
Del
OK
nexus.py
4.97 KB
Del
OK
nfs_export.py
4.92 KB
Del
OK
nftables.py
19.5 KB
Del
OK
npm.py
11.21 KB
Del
OK
ntp.py
2.12 KB
Del
OK
nxos.py
10.37 KB
Del
OK
nxos_upgrade.py
3.5 KB
Del
OK
openstack_config.py
3.26 KB
Del
OK
openvswitch_bridge.py
4.36 KB
Del
OK
openvswitch_db.py
2.24 KB
Del
OK
openvswitch_port.py
17.24 KB
Del
OK
opsgenie.py
4.07 KB
Del
OK
pagerduty.py
1.89 KB
Del
OK
pagerduty_escalation_policy.py
5.42 KB
Del
OK
pagerduty_schedule.py
6.09 KB
Del
OK
pagerduty_service.py
3.93 KB
Del
OK
pagerduty_user.py
1.18 KB
Del
OK
panos.py
48.13 KB
Del
OK
pbm.py
20.46 KB
Del
OK
pcs.py
36.46 KB
Del
OK
pdbedit.py
3.43 KB
Del
OK
pecl.py
3.65 KB
Del
OK
pip_state.py
38.55 KB
Del
OK
pkg.py
138.08 KB
Del
OK
pkgbuild.py
11.37 KB
Del
OK
pkgng.py
685 B
Del
OK
pkgrepo.py
27.53 KB
Del
OK
portage_config.py
5.01 KB
Del
OK
ports.py
5.65 KB
Del
OK
postgres_cluster.py
4.19 KB
Del
OK
postgres_database.py
6.08 KB
Del
OK
postgres_extension.py
5.68 KB
Del
OK
postgres_group.py
8.52 KB
Del
OK
postgres_initdb.py
2.84 KB
Del
OK
postgres_language.py
3.94 KB
Del
OK
postgres_privileges.py
7.86 KB
Del
OK
postgres_schema.py
4.34 KB
Del
OK
postgres_tablespace.py
6.62 KB
Del
OK
postgres_user.py
9.49 KB
Del
OK
powerpath.py
2.34 KB
Del
OK
probes.py
15.06 KB
Del
OK
process.py
1.32 KB
Del
OK
proxy.py
4.94 KB
Del
OK
pushover.py
3.13 KB
Del
OK
pyenv.py
6.07 KB
Del
OK
pyrax_queues.py
2.97 KB
Del
OK
quota.py
1.4 KB
Del
OK
rabbitmq_cluster.py
1.84 KB
Del
OK
rabbitmq_plugin.py
2.77 KB
Del
OK
rabbitmq_policy.py
4.59 KB
Del
OK
rabbitmq_upstream.py
7.9 KB
Del
OK
rabbitmq_user.py
8.89 KB
Del
OK
rabbitmq_vhost.py
3.04 KB
Del
OK
rbac_solaris.py
6.67 KB
Del
OK
rbenv.py
7.36 KB
Del
OK
rdp.py
1.28 KB
Del
OK
redismod.py
4.76 KB
Del
OK
reg.py
19.22 KB
Del
OK
restconf.py
6.41 KB
Del
OK
rsync.py
4.45 KB
Del
OK
rvm.py
6.56 KB
Del
OK
salt_proxy.py
1.34 KB
Del
OK
saltmod.py
33.12 KB
Del
OK
saltutil.py
8.91 KB
Del
OK
schedule.py
12.47 KB
Del
OK
selinux.py
18.61 KB
Del
OK
serverdensity_device.py
6.41 KB
Del
OK
service.py
37.89 KB
Del
OK
slack.py
4.98 KB
Del
OK
smartos.py
44.83 KB
Del
OK
smtp.py
2.3 KB
Del
OK
snapper.py
7.24 KB
Del
OK
solrcloud.py
4.48 KB
Del
OK
splunk.py
4.32 KB
Del
OK
splunk_search.py
3.17 KB
Del
OK
sqlite3.py
14.7 KB
Del
OK
ssh_auth.py
19.57 KB
Del
OK
ssh_known_hosts.py
7.92 KB
Del
OK
stateconf.py
494 B
Del
OK
status.py
2.21 KB
Del
OK
statuspage.py
17.29 KB
Del
OK
supervisord.py
10.48 KB
Del
OK
svn.py
8.14 KB
Del
OK
sysctl.py
4.11 KB
Del
OK
sysfs.py
2.13 KB
Del
OK
syslog_ng.py
2.97 KB
Del
OK
sysrc.py
2.82 KB
Del
OK
telemetry_alert.py
7.04 KB
Del
OK
test.py
13.09 KB
Del
OK
testinframod.py
1.35 KB
Del
OK
timezone.py
3.42 KB
Del
OK
tls.py
1.81 KB
Del
OK
tomcat.py
9.72 KB
Del
OK
trafficserver.py
8.82 KB
Del
OK
tuned.py
3.32 KB
Del
OK
uptime.py
1.87 KB
Del
OK
user.py
38.63 KB
Del
OK
vagrant.py
11.4 KB
Del
OK
vault.py
3.28 KB
Del
OK
vbox_guest.py
4.05 KB
Del
OK
victorops.py
3.32 KB
Del
OK
virt.py
80.41 KB
Del
OK
virtualenv_mod.py
11.21 KB
Del
OK
webutil.py
3.89 KB
Del
OK
win_certutil.py
4.8 KB
Del
OK
win_dacl.py
7.96 KB
Del
OK
win_dism.py
14.97 KB
Del
OK
win_dns_client.py
8.32 KB
Del
OK
win_firewall.py
6.87 KB
Del
OK
win_iis.py
31.56 KB
Del
OK
win_lgpo.py
24.99 KB
Del
OK
win_lgpo_reg.py
10.96 KB
Del
OK
win_license.py
1.6 KB
Del
OK
win_network.py
14.18 KB
Del
OK
win_path.py
6.39 KB
Del
OK
win_pki.py
5.56 KB
Del
OK
win_powercfg.py
3.79 KB
Del
OK
win_servermanager.py
10.4 KB
Del
OK
win_shortcut.py
7.81 KB
Del
OK
win_smtp_server.py
10.01 KB
Del
OK
win_snmp.py
6.64 KB
Del
OK
win_system.py
13.78 KB
Del
OK
win_wua.py
16.27 KB
Del
OK
win_wusa.py
3.53 KB
Del
OK
winrepo.py
2.74 KB
Del
OK
wordpress.py
4.82 KB
Del
OK
x509.py
27.86 KB
Del
OK
x509_v2.py
64.78 KB
Del
OK
xml.py
1.75 KB
Del
OK
xmpp.py
2.61 KB
Del
OK
zabbix_action.py
9.35 KB
Del
OK
zabbix_host.py
27.25 KB
Del
OK
zabbix_hostgroup.py
5.64 KB
Del
OK
zabbix_mediatype.py
16.89 KB
Del
OK
zabbix_template.py
35.14 KB
Del
OK
zabbix_user.py
17.6 KB
Del
OK
zabbix_usergroup.py
9.64 KB
Del
OK
zabbix_usermacro.py
9.69 KB
Del
OK
zabbix_valuemap.py
8.11 KB
Del
OK
zcbuildout.py
5.16 KB
Del
OK
zenoss.py
2.89 KB
Del
OK
zfs.py
34.48 KB
Del
OK
zk_concurrency.py
5.81 KB
Del
OK
zone.py
46.48 KB
Del
OK
zookeeper.py
11.55 KB
Del
OK
zpool.py
13.4 KB
Del
OK
Edit: smartos.py
""" 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] = '"{}"'.format(config[prop]) config_file.write( salt.utils.stringutils.to_str("{}={}\n".format(prop, config[prop])) ) 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 = "docker:{}".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("{}=".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 str(config[name]) == str(value): # we're good ret["result"] = True ret["comment"] = 'property {} already has value "{}"'.format(name, value) else: # update property ret["result"] = True ret["comment"] = 'updated property {} with value "{}"'.format(name, value) ret["changes"][name] = value config[name] = value else: # add property ret["result"] = True ret["comment"] = 'added property {} with value "{}"'.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) 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"] = "property {} deleted".format(name) ret["changes"][name] = None del config[name] else: # we're good ret["result"] = True ret["comment"] = "property {} 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 {} 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 {} added".format(name) ret["changes"][name] = "added" else: ret["comment"] = "image source {} not added".format(name) 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"] = "image source {} 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 {} deleted".format(name) ret["changes"][name] = "deleted" else: ret["comment"] = "image source {} not deleted".format(name) 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"] = "image {} 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 {} imported".format(name) ret["changes"] = res else: ret["comment"] = "image {} was unable to be imported".format(name) else: ret["result"] = False ret["comment"] = "image {} 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 {} 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 {} 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 {} and {} children deleted".format( name, image_count ) else: ret["comment"] = "image {} 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", "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["enforce_{}".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_{}".format(collection) not in vmconfig["changed"]: vmconfig["changed"]["set_{}".format(collection)] = {} # add property to changeset vmconfig["changed"]["set_{}".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_{}".format(collection) not in vmconfig["changed"]: vmconfig["changed"]["remove_{}".format(collection)] = [] # remove property vmconfig["changed"]["remove_{}".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_{}".format(instance) not in vmconfig["changed"] ): vmconfig["changed"][ "update_{}".format(instance) ] = [] update_cfg[ vmconfig_type["instance"][instance] ] = state_cfg[vmconfig_type["instance"][instance]] vmconfig["changed"][ "update_{}".format(instance) ].append(update_cfg) if add_instance: # create add_ array if "add_{}".format(instance) not in vmconfig["changed"]: vmconfig["changed"]["add_{}".format(instance)] = [] # add instance vmconfig["changed"]["add_{}".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_{}".format(instance) not in vmconfig["changed"]: vmconfig["changed"]["remove_{}".format(instance)] = [] # remove instance vmconfig["changed"]["remove_{}".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"] = "{}".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"] = "vm {} 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 {}".format(name) else: ret["comment"] = "vm {} 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 {} 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 {}".format(name) else: ret["changes"][name] = "running" ret["comment"] = "vm {} 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 {} 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 {}".format(name) else: ret["changes"][name] = "stopped" ret["comment"] = "vm {} stopped".format(name) return ret
Save