Skip to content

Commit 5b7d951

Browse files
authored
Implement --check support for the libvirt.virt module (ansible-collections#183)
* Implement check mode for the Virt class * Implement domain define with check mode * Implement domain undefine with check mode * Enable check mode * Add sanity tests for domain
1 parent a0ba856 commit 5b7d951

File tree

14 files changed

+295
-19
lines changed

14 files changed

+295
-19
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
minor_changes:
2+
- virt - implement basic check mode functionality (https://github.com/ansible-collections/community.libvirt/issue/98)

plugins/modules/virt.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848
- community.libvirt.virt.options_command
4949
- community.libvirt.virt.options_mutate_flags
5050
- community.libvirt.requirements
51+
attributes:
52+
check_mode:
53+
description: Supports check_mode.
54+
support: full
5155
author:
5256
- Ansible Core Team
5357
- Michael DeHaan
@@ -395,6 +399,9 @@ def virttype(self):
395399

396400
def autostart(self, vmid, as_flag):
397401
self.conn = self.__get_conn()
402+
if self.module.check_mode:
403+
return self.conn.get_autostart(vmid) != as_flag
404+
398405
# Change autostart flag only if needed
399406
if self.conn.get_autostart(vmid) != as_flag:
400407
self.conn.set_autostart(vmid, as_flag)
@@ -408,44 +415,60 @@ def freemem(self):
408415

409416
def shutdown(self, vmid):
410417
""" Make the machine with the given vmid stop running. Whatever that takes. """
418+
if self.module.check_mode:
419+
return 0
411420
self.__get_conn()
412421
self.conn.shutdown(vmid)
413422
return 0
414423

415424
def pause(self, vmid):
416425
""" Pause the machine with the given vmid. """
417-
426+
if self.module.check_mode:
427+
return 0
418428
self.__get_conn()
419429
return self.conn.suspend(vmid)
420430

421431
def unpause(self, vmid):
422432
""" Unpause the machine with the given vmid. """
423-
433+
if self.module.check_mode:
434+
return 0
424435
self.__get_conn()
425436
return self.conn.resume(vmid)
426437

427438
def create(self, vmid):
428439
""" Start the machine via the given vmid """
429-
440+
if self.module.check_mode:
441+
return 0
430442
self.__get_conn()
431443
return self.conn.create(vmid)
432444

433445
def start(self, vmid):
434446
""" Start the machine via the given id/name """
435-
447+
if self.module.check_mode:
448+
return 0
436449
self.__get_conn()
437450
return self.conn.create(vmid)
438451

439452
def destroy(self, vmid):
440453
""" Pull the virtual power from the virtual domain, giving it virtually no time to virtually shut down. """
454+
if self.module.check_mode:
455+
return 0
441456
self.__get_conn()
442457
return self.conn.destroy(vmid)
443458

444459
def undefine(self, vmid, flag):
445460
""" Stop a domain, and then wipe it from the face of the earth. (delete disk/config file) """
446-
461+
if self.module.check_mode:
462+
return {
463+
'changed': vmid in self.list_vms(),
464+
'command': 0,
465+
}
447466
self.__get_conn()
448-
return self.conn.undefine(vmid, flag)
467+
res = self.conn.undefine(vmid, flag)
468+
return {
469+
'changed': res == 0,
470+
'command': res,
471+
}
449472

450473
def status(self, vmid):
451474
"""
@@ -483,6 +506,8 @@ def define(self, xml):
483506
"""
484507
Define a guest with the given xml
485508
"""
509+
if self.module.check_mode:
510+
return 0
486511
self.__get_conn()
487512
return self.conn.define_from_xml(xml)
488513

@@ -511,11 +536,12 @@ def handle_define(module, v):
511536
guest = module.params.get('name', None)
512537
autostart = module.params.get('autostart', None)
513538
mutate_flags = module.params.get('mutate_flags', [])
539+
parser = etree.XMLParser(remove_blank_text=True)
514540

515541
if not xml:
516542
module.fail_json(msg="define requires 'xml' argument")
517543
try:
518-
incoming_xml = etree.fromstring(xml)
544+
incoming_xml = etree.fromstring(xml, parser)
519545
except etree.XMLSyntaxError:
520546
# TODO: provide info from parser
521547
module.fail_json(msg="given XML is invalid")
@@ -557,7 +583,7 @@ def handle_define(module, v):
557583
try:
558584
existing_domain = v.get_vm(domain_name)
559585
existing_xml_raw = existing_domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)
560-
existing_xml = etree.fromstring(existing_xml_raw)
586+
existing_xml = etree.fromstring(existing_xml_raw, parser)
561587
except VMNotFound:
562588
existing_domain = None
563589
existing_xml_raw = None
@@ -651,9 +677,19 @@ def get_interface_count(_type, source=None):
651677
))
652678

653679
try:
654-
domain_xml = etree.tostring(incoming_xml).decode()
680+
domain_xml = etree.tostring(incoming_xml, pretty_print=True).decode()
681+
682+
if module.check_mode:
683+
before = etree.tostring(existing_xml, pretty_print=True).decode() if existing_xml else ''
684+
res.update({
685+
'changed': before != domain_xml,
686+
'diff': {
687+
'before': before,
688+
'after': domain_xml
689+
},
690+
})
691+
return res
655692

656-
# TODO: support check mode
657693
domain = v.define(domain_xml)
658694

659695
if existing_domain is not None:
@@ -745,6 +781,12 @@ def core(module):
745781
return VIRT_SUCCESS, res
746782

747783
if command:
784+
def exec_virt(*args):
785+
res = getattr(v, command)(*args)
786+
if not isinstance(res, dict):
787+
res = {command: res}
788+
return res
789+
748790
if command in VM_COMMANDS:
749791
if command == 'define':
750792
res.update(handle_define(module, v))
@@ -771,24 +813,18 @@ def core(module):
771813
elif force is True:
772814
flag = 23
773815
# Finally, execute with flag
774-
res = getattr(v, command)(guest, flag)
775-
if not isinstance(res, dict):
776-
res = {command: res}
816+
res = exec_virt(guest, flag)
777817

778818
elif command == 'uuid':
779819
res = {'uuid': v.get_uuid(guest)}
780820

781821
else:
782-
res = getattr(v, command)(guest)
783-
if not isinstance(res, dict):
784-
res = {command: res}
822+
res = exec_virt(guest)
785823

786824
return VIRT_SUCCESS, res
787825

788826
elif hasattr(v, command):
789-
res = getattr(v, command)()
790-
if not isinstance(res, dict):
791-
res = {command: res}
827+
res = exec_virt()
792828
return VIRT_SUCCESS, res
793829

794830
else:
@@ -810,6 +846,7 @@ def main():
810846
xml=dict(type='str'),
811847
mutate_flags=dict(type='list', elements='str', choices=MUTATE_FLAGS, default=['ADD_UUID']),
812848
),
849+
supports_check_mode=True
813850
)
814851

815852
if not HAS_VIRT:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
shippable/posix/group1
2+
skip/aix
3+
skip/freebsd
4+
skip/osx
5+
needs/privileged
6+
destructive
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
emulator_bin: /usr/bin/qemu-system-x86_64
3+
domain_info:
4+
name: test_dom
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
- name: "Determine QEMU version"
3+
command: "{{ emulator_bin }} --version"
4+
register: emulator_version_output
5+
6+
- name: "Set QEMU version"
7+
set_fact:
8+
emulator_version: "{{ 0 | extract(emulator_version_output.stdout | regex_search('version (\\d+\\.\\d+)', '\\1')) }}"
9+
10+
#
11+
# Define domain
12+
#
13+
- name: "Define {{ domain_info.name }} on check_mode(pre)"
14+
community.libvirt.virt:
15+
command: define
16+
name: "{{ domain_info.name }}"
17+
xml: '{{ lookup("template", "test_domain.xml.j2") }}'
18+
register: result_pre
19+
check_mode: true
20+
21+
- name: "Define {{ domain_info.name }}"
22+
community.libvirt.virt:
23+
command: define
24+
name: "{{ domain_info.name }}"
25+
xml: '{{ lookup("template", "test_domain.xml.j2") }}'
26+
register: result
27+
28+
- name: "Define {{ domain_info.name }} on check_mode(post)"
29+
community.libvirt.virt:
30+
command: define
31+
name: "{{ domain_info.name }}"
32+
xml: '{{ lookup("template", "test_domain.xml.j2") }}'
33+
register: result_post
34+
check_mode: true
35+
36+
- name: "Ensure the {{ domain_info.name }} has been defined"
37+
assert:
38+
that:
39+
- result_pre is changed
40+
- result is changed
41+
- result_post is not changed
42+
- result.created == domain_info.name
43+
44+
#
45+
# Start domain
46+
#
47+
- name: "Start {{ domain_info.name }} on check_mode(pre)"
48+
community.libvirt.virt:
49+
state: running
50+
name: "{{ domain_info.name }}"
51+
register: result_pre
52+
check_mode: true
53+
54+
- name: "Start {{ domain_info.name }}"
55+
community.libvirt.virt:
56+
state: running
57+
name: "{{ domain_info.name }}"
58+
register: result
59+
60+
- name: "Start {{ domain_info.name }} on check_mode(post}"
61+
community.libvirt.virt:
62+
state: running
63+
name: "{{ domain_info.name }}"
64+
register: result_post
65+
check_mode: true
66+
67+
- name: "Ensure the {{ domain_info.name }} has started"
68+
assert:
69+
that:
70+
- result_pre is changed
71+
- result is changed and result.msg == 0
72+
- result_post is not changed
73+
74+
#
75+
# Undefine domain
76+
#
77+
- name: Delete "{{ domain_info.name }} on check_mode(pre)"
78+
community.libvirt.virt:
79+
command: undefine
80+
name: "{{ domain_info.name }}"
81+
register: result_pre
82+
check_mode: true
83+
84+
- name: Delete "{{ domain_info.name }}"
85+
community.libvirt.virt:
86+
command: undefine
87+
name: "{{ domain_info.name }}"
88+
register: result
89+
90+
- name: Delete "{{ domain_info.name }} on check_mode(post)"
91+
community.libvirt.virt:
92+
command: undefine
93+
name: "{{ domain_info.name }}"
94+
register: result_post
95+
check_mode: true
96+
97+
- name: "Ensure the {{ domain_info.name }} has been deleted"
98+
assert:
99+
that:
100+
- result_pre is changed
101+
- result is changed and result.command == 0
102+
- result_post is not changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
- include_vars: '{{ item }}'
3+
with_first_found:
4+
- "{{ ansible_distribution }}-{{ ansible_distribution_version}}.yml"
5+
- "{{ ansible_distribution }}-{{ ansible_distribution_major_version}}.yml"
6+
- "{{ ansible_distribution }}.yml"
7+
- "default.yml"
8+
9+
- block:
10+
- name: Install libvirt packages
11+
package:
12+
name: "{{ virt_domain_packages }}"
13+
14+
- name: Start libvirt service
15+
service:
16+
name: libvirtd
17+
state: started
18+
19+
- name: Test check mode for domain defines
20+
import_tasks: define_check_mode.yml
21+
22+
always:
23+
- name: Stop libvirt
24+
service:
25+
name: libvirtd
26+
state: stopped
27+
28+
- name: Remove only the libvirt packages
29+
package:
30+
name: "{{ virt_domain_packages|select('match', '.*libvirt.*')|list }}"
31+
state: absent
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<domain type='qemu'>
2+
<name>{{ domain_info['name'] }}</name>
3+
<uuid>125388f2-6ab4-4343-a166-bad6ffad017e</uuid>
4+
<memory unit='KiB'>4000768</memory>
5+
<currentMemory unit='KiB'>4000000</currentMemory>
6+
<vcpu placement='static'>1</vcpu>
7+
<os>
8+
<type arch='x86_64' machine='pc-i440fx-{{ emulator_version }}'>hvm</type>
9+
<boot dev='hd'/>
10+
</os>
11+
<features>
12+
<acpi/>
13+
<apic/>
14+
<pae/>
15+
</features>
16+
<cpu mode='custom' match='exact' check='none'>
17+
<model fallback='forbid'>qemu64</model>
18+
</cpu>
19+
<clock offset='utc'>
20+
<timer name='kvmclock' present='no'/>
21+
</clock>
22+
<on_poweroff>destroy</on_poweroff>
23+
<on_reboot>restart</on_reboot>
24+
<on_crash>restart</on_crash>
25+
<devices>
26+
<emulator>{{ emulator_bin }}</emulator>
27+
<controller type='usb' index='0' model='piix3-uhci'>
28+
<address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/>
29+
</controller>
30+
<controller type='pci' index='0' model='pci-root'/>
31+
<controller type='pci' index='1' model='pci-bridge'>
32+
<model name='pci-bridge'/>
33+
<target chassisNr='1'/>
34+
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
35+
</controller>
36+
<controller type='pci' index='2' model='pci-bridge'>
37+
<model name='pci-bridge'/>
38+
<target chassisNr='2'/>
39+
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
40+
</controller>
41+
<controller type='pci' index='3' model='pci-bridge'>
42+
<model name='pci-bridge'/>
43+
<target chassisNr='3'/>
44+
<address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
45+
</controller>
46+
<input type='mouse' bus='ps2'/>
47+
<input type='keyboard' bus='ps2'/>
48+
<audio id='1' type='none'/>
49+
<memballoon model='none'/>
50+
</devices>
51+
</domain>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
virt_domain_packages:
3+
- libvirt-daemon
4+
- libvirt-daemon-system
5+
- python-libvirt
6+
- python-lxml

0 commit comments

Comments
 (0)