1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Jasper Lievisse Adriaanse <j@jasper.la>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: vmadm
short_description: Manage SmartOS virtual machines and zones
description:
- Manage SmartOS virtual machines through vmadm(1M).
author: Jasper Lievisse Adriaanse (@jasperla)
extends_documentation_fragment:
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
archive_on_delete:
required: false
description:
- When enabled, the zone dataset will be mounted on C(/zones/archive)
upon removal.
type: bool
autoboot:
required: false
description:
- Whether or not a VM is booted when the system is rebooted.
type: bool
brand:
choices: [ joyent, joyent-minimal, lx, kvm, bhyve ]
default: joyent
description:
- Type of virtual machine. The V(bhyve) option was added in community.general 0.2.0.
type: str
boot:
required: false
description:
- Set the boot order for KVM VMs.
type: str
cpu_cap:
required: false
description:
- Sets a limit on the amount of CPU time that can be used by a VM.
Use V(0) for no cap.
type: int
cpu_shares:
required: false
description:
- Sets a limit on the number of fair share scheduler (FSS) CPU shares for
a VM. This limit is relative to all other VMs on the system.
type: int
cpu_type:
required: false
choices: [ qemu64, host ]
default: qemu64
description:
- Control the type of virtual CPU exposed to KVM VMs.
type: str
customer_metadata:
required: false
description:
- Metadata to be set and associated with this VM, this contain customer
modifiable keys.
type: dict
delegate_dataset:
required: false
description:
- Whether to delegate a ZFS dataset to an OS VM.
type: bool
disk_driver:
required: false
description:
- Default value for a virtual disk model for KVM guests.
type: str
disks:
required: false
description:
- A list of disks to add, valid properties are documented in vmadm(1M).
type: list
elements: dict
dns_domain:
required: false
description:
- Domain value for C(/etc/hosts).
type: str
docker:
required: false
description:
- Docker images need this flag enabled along with the O(brand) set to C(lx).
type: bool
filesystems:
required: false
description:
- Mount additional filesystems into an OS VM.
type: list
elements: dict
firewall_enabled:
required: false
description:
- Enables the firewall, allowing fwadm(1M) rules to be applied.
type: bool
force:
required: false
description:
- Force a particular action (i.e. stop or delete a VM).
type: bool
fs_allowed:
required: false
description:
- Comma separated list of filesystem types this zone is allowed to mount.
type: str
hostname:
required: false
description:
- Zone/VM hostname.
type: str
image_uuid:
required: false
description:
- Image UUID.
type: str
indestructible_delegated:
required: false
description:
- Adds an C(@indestructible) snapshot to delegated datasets.
type: bool
indestructible_zoneroot:
required: false
description:
- Adds an C(@indestructible) snapshot to zoneroot.
type: bool
internal_metadata:
required: false
description:
- Metadata to be set and associated with this VM, this contains operator
generated keys.
type: dict
internal_metadata_namespace:
required: false
description:
- List of namespaces to be set as C(internal_metadata-only); these namespaces
will come from O(internal_metadata) rather than O(customer_metadata).
type: str
kernel_version:
required: false
description:
- Kernel version to emulate for LX VMs.
type: str
limit_priv:
required: false
description:
- Set (comma separated) list of privileges the zone is allowed to use.
type: str
maintain_resolvers:
required: false
description:
- Resolvers in C(/etc/resolv.conf) will be updated when updating
the O(resolvers) property.
type: bool
max_locked_memory:
required: false
description:
- Total amount of memory (in MiBs) on the host that can be locked by this VM.
type: int
max_lwps:
required: false
description:
- Maximum number of lightweight processes this VM is allowed to have running.
type: int
max_physical_memory:
required: false
description:
- Maximum amount of memory (in MiBs) on the host that the VM is allowed to use.
type: int
max_swap:
required: false
description:
- Maximum amount of virtual memory (in MiBs) the VM is allowed to use.
type: int
mdata_exec_timeout:
required: false
description:
- Timeout in seconds (or 0 to disable) for the C(svc:/smartdc/mdata:execute) service
that runs user-scripts in the zone.
type: int
name:
required: false
aliases: [ alias ]
description:
- Name of the VM. vmadm(1M) uses this as an optional name.
type: str
nic_driver:
required: false
description:
- Default value for a virtual NIC model for KVM guests.
type: str
nics:
required: false
description:
- A list of nics to add, valid properties are documented in vmadm(1M).
type: list
elements: dict
nowait:
required: false
description:
- Consider the provisioning complete when the VM first starts, rather than
when the VM has rebooted.
type: bool
qemu_opts:
required: false
description:
- Additional qemu arguments for KVM guests. This overwrites the default arguments
provided by vmadm(1M) and should only be used for debugging.
type: str
qemu_extra_opts:
required: false
description:
- Additional qemu cmdline arguments for KVM guests.
type: str
quota:
required: false
description:
- Quota on zone filesystems (in MiBs).
type: int
ram:
required: false
description:
- Amount of virtual RAM for a KVM guest (in MiBs).
type: int
resolvers:
required: false
description:
- List of resolvers to be put into C(/etc/resolv.conf).
type: list
elements: str
routes:
required: false
description:
- Dictionary that maps destinations to gateways, these will be set as static
routes in the VM.
type: dict
spice_opts:
required: false
description:
- Addition options for SPICE-enabled KVM VMs.
type: str
spice_password:
required: false
description:
- Password required to connect to SPICE. By default no password is set.
Please note this can be read from the Global Zone.
type: str
state:
choices: [ present, running, absent, deleted, stopped, created, restarted, rebooted ]
default: running
description:
- States for the VM to be in. Please note that V(present), V(stopped) and V(restarted)
operate on a VM that is currently provisioned. V(present) means that the VM will be
created if it was absent, and that it will be in a running state. V(absent) will
shutdown the zone before removing it.
V(stopped) means the zone will be created if it does not exist already, before shutting
it down.
type: str
tmpfs:
required: false
description:
- Amount of memory (in MiBs) that will be available in the VM for the C(/tmp) filesystem.
type: int
uuid:
required: false
description:
- UUID of the VM. Can either be a full UUID or V(*) for all VMs.
type: str
vcpus:
required: false
description:
- Number of virtual CPUs for a KVM guest.
type: int
vga:
required: false
description:
- Specify VGA emulation used by KVM VMs.
type: str
virtio_txburst:
required: false
description:
- Number of packets that can be sent in a single flush of the tx queue of virtio NICs.
type: int
virtio_txtimer:
required: false
description:
- Timeout (in nanoseconds) for the TX timer of virtio NICs.
type: int
vnc_password:
required: false
description:
- Password required to connect to VNC. By default no password is set.
Please note this can be read from the Global Zone.
type: str
vnc_port:
required: false
description:
- TCP port to listen of the VNC server. Or set V(0) for random,
or V(-1) to disable.
type: int
zfs_data_compression:
required: false
description:
- Specifies compression algorithm used for this VMs data dataset. This option
only has effect on delegated datasets.
type: str
zfs_data_recsize:
required: false
description:
- Suggested block size (power of 2) for files in the delegated dataset's filesystem.
type: int
zfs_filesystem_limit:
required: false
description:
- Maximum number of filesystems the VM can have.
type: int
zfs_io_priority:
required: false
description:
- IO throttle priority value relative to other VMs.
type: int
zfs_root_compression:
required: false
description:
- Specifies compression algorithm used for this VMs root dataset. This option
only has effect on the zoneroot dataset.
type: str
zfs_root_recsize:
required: false
description:
- Suggested block size (power of 2) for files in the zoneroot dataset's filesystem.
type: int
zfs_snapshot_limit:
required: false
description:
- Number of snapshots the VM can have.
type: int
zpool:
required: false
description:
- ZFS pool the VM's zone dataset will be created in.
type: str
'''
EXAMPLES = '''
- name: Create SmartOS zone
community.general.vmadm:
brand: joyent
state: present
alias: fw_zone
image_uuid: 95f265b8-96b2-11e6-9597-972f3af4b6d5
firewall_enabled: true
indestructible_zoneroot: true
nics:
- nic_tag: admin
ip: dhcp
primary: true
internal_metadata:
root_pw: 'secret'
quota: 1
- name: Delete a zone
community.general.vmadm:
alias: test_zone
state: deleted
- name: Stop all zones
community.general.vmadm:
uuid: '*'
state: stopped
'''
RETURN = '''
uuid:
description: UUID of the managed VM.
returned: always
type: str
sample: 'b217ab0b-cf57-efd8-cd85-958d0b80be33'
alias:
description: Alias of the managed VM.
returned: When addressing a VM by alias.
type: str
sample: 'dns-zone'
state:
description: State of the target, after execution.
returned: success
type: str
sample: 'running'
'''
import json
import os
import re
import tempfile
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native
# While vmadm(1M) supports a -E option to return any errors in JSON, the
# generated JSON does not play well with the JSON parsers of Python.
# The returned message contains '\n' as part of the stacktrace,
# which breaks the parsers.
def get_vm_prop(module, uuid, prop):
# Lookup a property for the given VM.
# Returns the property, or None if not found.
cmd = [module.vmadm, 'lookup', '-j', '-o', prop, 'uuid={0}'.format(uuid)]
(rc, stdout, stderr) = module.run_command(cmd)
if rc != 0:
module.fail_json(
msg='Could not perform lookup of {0} on {1}'.format(prop, uuid), exception=stderr)
try:
stdout_json = json.loads(stdout)
except Exception as e:
module.fail_json(
msg='Invalid JSON returned by vmadm for uuid lookup of {0}'.format(prop),
details=to_native(e), exception=traceback.format_exc())
if stdout_json:
return stdout_json[0].get(prop)
def get_vm_uuid(module, alias):
# Lookup the uuid that goes with the given alias.
# Returns the uuid or '' if not found.
cmd = [module.vmadm, 'lookup', '-j', '-o', 'uuid', 'alias={0}'.format(alias)]
(rc, stdout, stderr) = module.run_command(cmd)
if rc != 0:
module.fail_json(
msg='Could not retrieve UUID of {0}'.format(alias), exception=stderr)
# If no VM was found matching the given alias, we get back an empty array.
# That is not an error condition as we might be explicitly checking it's
# absence.
try:
stdout_json = json.loads(stdout)
except Exception as e:
module.fail_json(
msg='Invalid JSON returned by vmadm for uuid lookup of {0}'.format(alias),
details=to_native(e), exception=traceback.format_exc())
if stdout_json:
return stdout_json[0].get('uuid')
def get_all_vm_uuids(module):
# Retrieve the UUIDs for all VMs.
cmd = [module.vmadm, 'lookup', '-j', '-o', 'uuid']
(rc, stdout, stderr) = module.run_command(cmd)
if rc != 0:
module.fail_json(msg='Failed to get VMs list', exception=stderr)
try:
stdout_json = json.loads(stdout)
return [v['uuid'] for v in stdout_json]
except Exception as e:
module.fail_json(msg='Could not retrieve VM UUIDs', details=to_native(e),
exception=traceback.format_exc())
def new_vm(module, uuid, vm_state):
payload_file = create_payload(module, uuid)
(rc, dummy, stderr) = vmadm_create_vm(module, payload_file)
if rc != 0:
changed = False
module.fail_json(msg='Could not create VM', exception=stderr)
else:
changed = True
# 'vmadm create' returns all output to stderr...
match = re.match('Successfully created VM (.*)', stderr)
if match:
vm_uuid = match.groups()[0]
if not is_valid_uuid(vm_uuid):
module.fail_json(msg='Invalid UUID for VM {0}?'.format(vm_uuid))
else:
module.fail_json(msg='Could not retrieve UUID of newly created(?) VM')
# Now that the VM is created, ensure it is in the desired state (if not 'running')
if vm_state != 'running':
ret = set_vm_state(module, vm_uuid, vm_state)
if not ret:
module.fail_json(msg='Could not set VM {0} to state {1}'.format(vm_uuid, vm_state))
try:
os.unlink(payload_file)
except Exception as e:
# Since the payload may contain sensitive information, fail hard
# if we cannot remove the file so the operator knows about it.
module.fail_json(msg='Could not remove temporary JSON payload file {0}: {1}'.format(payload_file, to_native(e)),
exception=traceback.format_exc())
return changed, vm_uuid
def vmadm_create_vm(module, payload_file):
# Create a new VM using the provided payload.
cmd = [module.vmadm, 'create', '-f', payload_file]
return module.run_command(cmd)
def set_vm_state(module, vm_uuid, vm_state):
p = module.params
# Check if the VM is already in the desired state.
state = get_vm_prop(module, vm_uuid, 'state')
if state and (state == vm_state):
return None
# Lookup table for the state to be in, and which command to use for that.
# vm_state: [vmadm commandm, forceable?]
cmds = {
'stopped': ['stop', True],
'running': ['start', False],
'deleted': ['delete', True],
'rebooted': ['reboot', False]
}
command, forceable = cmds[vm_state]
force = ['-F'] if p['force'] and forceable else []
cmd = [module.vmadm, command] + force + [vm_uuid]
(dummy, dummy, stderr) = module.run_command(cmd)
match = re.match('^Successfully.*', stderr)
return match is not None
def create_payload(module, uuid):
# Create the JSON payload (vmdef) and return the filename.
# Filter out the few options that are not valid VM properties.
module_options = ['force', 'state']
# @TODO make this a simple {} comprehension as soon as py2 is ditched
# @TODO {k: v for k, v in p.items() if k not in module_options}
vmdef = dict([(k, v) for k, v in module.params.items() if k not in module_options and v])
try:
vmdef_json = json.dumps(vmdef)
except Exception as e:
module.fail_json(
msg='Could not create valid JSON payload', exception=traceback.format_exc())
# Create the temporary file that contains our payload, and set tight
# permissions for it may container sensitive information.
try:
# XXX: When there's a way to get the current ansible temporary directory
# drop the mkstemp call and rely on ANSIBLE_KEEP_REMOTE_FILES to retain
# the payload (thus removing the `save_payload` option).
fname = tempfile.mkstemp()[1]
os.chmod(fname, 0o400)
with open(fname, 'w') as fh:
fh.write(vmdef_json)
except Exception as e:
module.fail_json(msg='Could not save JSON payload: %s' % to_native(e), exception=traceback.format_exc())
return fname
def vm_state_transition(module, uuid, vm_state):
ret = set_vm_state(module, uuid, vm_state)
# Whether the VM changed state.
if ret is None:
return False
elif ret:
return True
else:
module.fail_json(msg='Failed to set VM {0} to state {1}'.format(uuid, vm_state))
def is_valid_uuid(uuid):
return re.match('^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$', uuid, re.IGNORECASE) is not None
def validate_uuids(module):
failed = [
name
for name, pvalue in [(x, module.params[x]) for x in ['uuid', 'image_uuid']]
if pvalue and pvalue != '*' and not is_valid_uuid(pvalue)
]
if failed:
module.fail_json(msg='No valid UUID(s) found for: {0}'.format(", ".join(failed)))
def manage_all_vms(module, vm_state):
# Handle operations for all VMs, which can by definition only
# be state transitions.
state = module.params['state']
if state == 'created':
module.fail_json(msg='State "created" is only valid for tasks with a single VM')
# If any of the VMs has a change, the task as a whole has a change.
any_changed = False
# First get all VM uuids and for each check their state, and adjust it if needed.
for uuid in get_all_vm_uuids(module):
current_vm_state = get_vm_prop(module, uuid, 'state')
if not current_vm_state and vm_state == 'deleted':
any_changed = False
else:
if module.check_mode:
if (not current_vm_state) or (get_vm_prop(module, uuid, 'state') != state):
any_changed = True
else:
any_changed = vm_state_transition(module, uuid, vm_state) or any_changed
return any_changed
def main():
# In order to reduce the clutter and boilerplate for trivial options,
# abstract the vmadm properties and build the dict of arguments later.
# Dict of all options that are simple to define based on their type.
# They're not required and have a default of None.
properties = {
'str': [
'boot', 'disk_driver', 'dns_domain', 'fs_allowed', 'hostname',
'image_uuid', 'internal_metadata_namespace', 'kernel_version',
'limit_priv', 'nic_driver', 'qemu_opts', 'qemu_extra_opts',
'spice_opts', 'uuid', 'vga', 'zfs_data_compression',
'zfs_root_compression', 'zpool'
],
'bool': [
'archive_on_delete', 'autoboot', 'delegate_dataset',
'docker', 'firewall_enabled', 'force', 'indestructible_delegated',
'indestructible_zoneroot', 'maintain_resolvers', 'nowait'
],
'int': [
'cpu_cap', 'cpu_shares', 'max_locked_memory', 'max_lwps',
'max_physical_memory', 'max_swap', 'mdata_exec_timeout',
'quota', 'ram', 'tmpfs', 'vcpus', 'virtio_txburst',
'virtio_txtimer', 'vnc_port', 'zfs_data_recsize',
'zfs_filesystem_limit', 'zfs_io_priority', 'zfs_root_recsize',
'zfs_snapshot_limit'
],
'dict': ['customer_metadata', 'internal_metadata', 'routes'],
}
# Start with the options that are not as trivial as those above.
options = dict(
state=dict(
default='running',
type='str',
choices=['present', 'running', 'absent', 'deleted', 'stopped', 'created', 'restarted', 'rebooted']
),
name=dict(
type='str',
aliases=['alias']
),
brand=dict(
default='joyent',
type='str',
choices=['joyent', 'joyent-minimal', 'lx', 'kvm', 'bhyve']
),
cpu_type=dict(
default='qemu64',
type='str',
choices=['host', 'qemu64']
),
# Regular strings, however these require additional options.
spice_password=dict(type='str', no_log=True),
vnc_password=dict(type='str', no_log=True),
disks=dict(type='list', elements='dict'),
nics=dict(type='list', elements='dict'),
resolvers=dict(type='list', elements='str'),
filesystems=dict(type='list', elements='dict'),
)
# Add our 'simple' options to options dict.
for type in properties:
for p in properties[type]:
option = dict(type=type)
options[p] = option
module = AnsibleModule(
argument_spec=options,
supports_check_mode=True,
required_one_of=[['name', 'uuid']]
)
module.vmadm = module.get_bin_path('vmadm', required=True)
p = module.params
uuid = p['uuid']
state = p['state']
# Translate the state parameter into something we can use later on.
if state in ['present', 'running']:
vm_state = 'running'
elif state in ['stopped', 'created']:
vm_state = 'stopped'
elif state in ['absent', 'deleted']:
vm_state = 'deleted'
elif state in ['restarted', 'rebooted']:
vm_state = 'rebooted'
result = {'state': state}
# While it's possible to refer to a given VM by it's `alias`, it's easier
# to operate on VMs by their UUID. So if we're not given a `uuid`, look
# it up.
if not uuid:
uuid = get_vm_uuid(module, p['name'])
# Bit of a chicken and egg problem here for VMs with state == deleted.
# If they're going to be removed in this play, we have to lookup the
# uuid. If they're already deleted there's nothing to lookup.
# So if state == deleted and get_vm_uuid() returned '', the VM is already
# deleted and there's nothing else to do.
if uuid is None and vm_state == 'deleted':
result['name'] = p['name']
module.exit_json(**result)
validate_uuids(module)
if p['name']:
result['name'] = p['name']
result['uuid'] = uuid
if uuid == '*':
result['changed'] = manage_all_vms(module, vm_state)
module.exit_json(**result)
# The general flow is as follows:
# - first the current state of the VM is obtained by it's UUID.
# - If the state was not found and the desired state is 'deleted', return.
# - If the state was not found, it means the VM has to be created.
# Subsequently the VM will be set to the desired state (i.e. stopped)
# - Otherwise, it means the VM exists already and we operate on it's
# state (i.e. reboot it.)
#
# In the future it should be possible to query the VM for a particular
# property as a valid state (i.e. queried) so the result can be
# registered.
# Also, VMs should be able to get their properties updated.
# Managing VM snapshots should be part of a standalone module.
# First obtain the VM state to determine what needs to be done with it.
current_vm_state = get_vm_prop(module, uuid, 'state')
# First handle the case where the VM should be deleted and is not present.
if not current_vm_state and vm_state == 'deleted':
result['changed'] = False
elif module.check_mode:
# Shortcut for check mode, if there is no VM yet, it will need to be created.
# Or, if the VM is not in the desired state yet, it needs to transition.
result['changed'] = (not current_vm_state) or (get_vm_prop(module, uuid, 'state') != state)
elif not current_vm_state:
# No VM was found that matched the given ID (alias or uuid), so we create it.
result['changed'], result['uuid'] = new_vm(module, uuid, vm_state)
else:
# VM was found, operate on its state directly.
result['changed'] = vm_state_transition(module, uuid, vm_state)
module.exit_json(**result)
if __name__ == '__main__':
main()
|