Repository: ansible/ansible

Issue:
cannot use autostart with command=define in virt module 
<!---
Verify first that your issue/request is not already reported on GitHub.
THIS FORM WILL BE READ BY A MACHINE, COMPLETE ALL SECTIONS AS DESCRIBED.
Also test if the latest release, and devel branch are affected too.
ALWAYS add information AFTER (OUTSIDE) these html comments.
Otherwise it may end up being automatically closed by our bot. -->

##### SUMMARY
<!--- Explain the problem briefly -->
```autostart``` cannot be used in virt module with ```command=define```. It fails with "Domain not found: no domain with matching name '<vm_name_here>'". This happens because of attempting to set ```autostart``` before defining the VM: https://github.com/ansible/ansible/blob/63f2ad8776314930a800076ef363f4e60a2ef1a4/lib/ansible/modules/cloud/misc/virt.py#L477
It seems logical to define a machine and set the autostart in one task.
Deeper look shows that ```autostart``` cannot be used alone - only with either ```state``` or ```command```. 
Additionally, if ```state``` is set, ```autostart``` will be ignored, while it seems logical to run ```state=running``` and ```autostart=yes``` in one task to ensure that VM runs now and will start on host reboot. 

On a side: the logic in core() function makes it quite hard to extend any functionality. Param dependencies are a bit loose too. With some (seemingly valid) combinations of params it fails, not even providing a reason. It might make sense to use validators provided with the AnsibleModule's init: mutually_exclusive=None, required_together=None etc.
Documentation lacks param dependencies and examples. E.g. it's not obvious that ```state=running``` and ```command=list_vms``` will list all running vms, while other combinations of ```state``` and ```command``` will just ignore the ```command``` silently.

##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
<!--- Insert, BELOW THIS COMMENT, the name of the module, plugin, task or feature.
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path-->
virt

##### ANSIBLE VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "ansible --version" between quotes below -->
```
ansible 2.5.2
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/user/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
```

##### CONFIGURATION
<!--- If using Ansible 2.4 or above, paste, BELOW THIS COMMENT, the results of "ansible-config dump --only-changed"
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).-->
ANSIBLE_NOCOWS(/etc/ansible/ansible.cfg) = True
ANSIBLE_PIPELINING(/etc/ansible/ansible.cfg) = True
DEFAULT_HOST_LIST(/etc/ansible/ansible.cfg) = [u'/etc/ansible/hosts']
HOST_KEY_CHECKING(/etc/ansible/ansible.cfg) = False

##### OS / ENVIRONMENT
<!--- Mention, BELOW THIS COMMENT, the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.-->
ansible: CentOS Linux release 7.4.1708 (Core)
managed machine: CentOS Linux release 7.4.1708 (Core)
libvirt on managed machine: libvirt-2.0.0-10.el7_3.9.x86_64

##### STEPS TO REPRODUCE
<!--- For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used. -->

Run the following task against a host with libvirt+kvm+qemu:
<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: "Define a VM and make it autostart"
  virt:
    name: test_vm
    autostart: yes
    command: define
    xml: "{{ lookup('template', 'vm_template.xml.j2') }}"
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
VM is defined and will autostart when host reboots 

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
task fails with "msg": "Domain not found: no domain with matching name 'test_vm'"

<!--- Paste verbatim command output between quotes below -->
```
The full traceback is:
Traceback (most recent call last):
  File "/tmp/ansible_CDu9Fc/ansible_module_virt.py", line 527, in main
    rc, result = core(module)
  File "/tmp/ansible_CDu9Fc/ansible_module_virt.py", line 477, in core
    if autostart is not None and v.autostart(guest, autostart):
  File "/tmp/ansible_CDu9Fc/ansible_module_virt.py", line 340, in autostart
    if self.conn.get_autostart(vmid) != as_flag:
  File "/tmp/ansible_CDu9Fc/ansible_module_virt.py", line 249, in get_autostart
    vm = self.conn.lookupByName(vmid)
  File "/usr/lib64/python2.7/site-packages/libvirt.py", line 4227, in lookupByName
    if ret is None:raise libvirtError('virDomainLookupByName() failed', conn=self)
libvirtError: Domain not found: no domain with matching name 'test_vm'

fatal: [kvm01.ams]: FAILED! => {
    "changed": false, 
    "invocation": {
        "module_args": {
            "autostart": true, 
            "command": "define", 
            "name": "test_vm", 
            "state": null, 
            "uri": "qemu:///system", 
            "xml": "<domain type='kvm'>\n  <name>test_vm</name>\n ... and the rest of the xml ...</domain>\n\n"
        }
    }, 
    "msg": "Domain not found: no domain with matching name 'test_vm'"
}
```



Discussion:
Files identified in the description:
* [lib/ansible/modules/cloud/misc/virt.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/misc/virt.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
@nesanton Thanks for reporting this issue. This is already reported #34559 and PR is available https://github.com/ansible/ansible/pull/40457 
Would you be interested to test PR ? 

Resolving patch:
diff --git a/lib/ansible/modules/cloud/misc/virt.py b/lib/ansible/modules/cloud/misc/virt.py
--- a/lib/ansible/modules/cloud/misc/virt.py
+++ b/lib/ansible/modules/cloud/misc/virt.py
@@ -25,12 +25,14 @@
     description:
       - name of the guest VM being managed. Note that VM must be previously
         defined with xml.
-    required: true
+      - This option is required unless I(command) is C(list_vms).
   state:
     description:
       - Note that there may be some lag for state requests like C(shutdown)
         since these refer only to VM states. After starting a guest, it may not
         be immediately accessible.
+        state and command are mutually exclusive except when command=list_vms. In
+        this case all VMs in specified state will be listed.
     choices: [ destroyed, paused, running, shutdown ]
   command:
     description:
@@ -69,20 +71,42 @@
 # ansible host -m virt -a "name=alpha command=get_xml"
 # ansible host -m virt -a "name=alpha command=create uri=lxc:///"
 
----
-# a playbook example of defining and launching an LXC guest
-tasks:
-  - name: define vm
-    virt:
-        name: foo
-        command: define
-        xml: "{{ lookup('template', 'container-template.xml.j2') }}"
-        uri: 'lxc:///'
-  - name: start vm
-    virt:
-        name: foo
-        state: running
-        uri: 'lxc:///'
+# defining and launching an LXC guest
+- name: define vm
+  virt:
+    command: define
+    xml: "{{ lookup('template', 'container-template.xml.j2') }}"
+    uri: 'lxc:///'
+- name: start vm
+  virt:
+    name: foo
+    state: running
+    uri: 'lxc:///'
+
+# setting autostart on a qemu VM (default uri)
+- name: set autostart for a VM
+  virt:
+    name: foo
+    autostart: yes
+
+# Defining a VM and making is autostart with host. VM will be off after this task
+- name: define vm from xml and set autostart
+  virt:
+    command: define
+    xml: "{{ lookup('template', 'vm_template.xml.j2') }}"
+    autostart: yes
+
+# Listing VMs
+- name: list all VMs
+  virt:
+    command: list_vms
+  register: all_vms
+
+- name: list only running VMs
+  virt:
+    command: list_vms
+    state: running
+  register: running_vms
 '''
 
 RETURN = '''
@@ -107,11 +131,14 @@
 
 try:
     import libvirt
+    from libvirt import libvirtError
 except ImportError:
     HAS_VIRT = False
 else:
     HAS_VIRT = True
 
+import re
+
 from ansible.module_utils.basic import AnsibleModule
 from ansible.module_utils._text import to_native
 
@@ -446,6 +473,17 @@ def core(module):
             res = {command: res}
         return VIRT_SUCCESS, res
 
+    if autostart is not None and command != 'define':
+        if not guest:
+            module.fail_json(msg="autostart requires 1 argument: name")
+        try:
+            v.get_vm(guest)
+        except VMNotFound:
+            module.fail_json(msg="domain %s not found" % guest)
+        res['changed'] = v.autostart(guest, autostart)
+        if not command and not state:
+            return VIRT_SUCCESS, res
+
     if state:
         if not guest:
             module.fail_json(msg="state change requires a guest specified")
@@ -474,25 +512,53 @@ def core(module):
 
         return VIRT_SUCCESS, res
 
-    if autostart is not None and v.autostart(guest, autostart):
-        res['changed'] = True
-
     if command:
         if command in VM_COMMANDS:
-            if not guest:
-                module.fail_json(msg="%s requires 1 argument: guest" % command)
             if command == 'define':
                 if not xml:
                     module.fail_json(msg="define requires xml argument")
+                if guest:
+                    # there might be a mismatch between quest 'name' in the module and in the xml
+                    module.warn("'xml' is given - ignoring 'name'")
+                found_name = re.search('<name>(.*)</name>', xml).groups()
+                if found_name:
+                    domain_name = found_name[0]
+                else:
+                    module.fail_json(msg="Could not find domain 'name' in xml")
+
+                # From libvirt docs (https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainDefineXML):
+                # -- A previous definition for this domain would be overridden if it already exists.
+                #
+                # In real world testing with libvirt versions 1.2.17-13, 2.0.0-10 and 3.9.0-14
+                # on qemu and lxc domains results in:
+                # operation failed: domain '<name>' already exists with <uuid>
+                #
+                # In case a domain would be indeed overwritten, we should protect idempotency:
                 try:
-                    v.get_vm(guest)
+                    existing_domain = v.get_vm(domain_name)
                 except VMNotFound:
-                    v.define(xml)
-                    res = {'changed': True, 'created': guest}
-                return VIRT_SUCCESS, res
-            res = getattr(v, command)(guest)
-            if not isinstance(res, dict):
-                res = {command: res}
+                    existing_domain = None
+                try:
+                    domain = v.define(xml)
+                    if existing_domain:
+                        # if we are here, then libvirt redefined existing domain as the doc promised
+                        if existing_domain.XMLDesc() != domain.XMLDesc():
+                            res = {'changed': True, 'change_reason': 'config changed'}
+                    else:
+                        res = {'changed': True, 'created': domain.name()}
+                except libvirtError as e:
+                    if e.get_error_code() != 9:  # 9 means 'domain already exists' error
+                        module.fail_json(msg='libvirtError: %s' % e.message)
+                if autostart is not None and v.autostart(domain_name, autostart):
+                    res = {'changed': True, 'change_reason': 'autostart'}
+
+            elif not guest:
+                module.fail_json(msg="%s requires 1 argument: guest" % command)
+            else:
+                res = getattr(v, command)(guest)
+                if not isinstance(res, dict):
+                    res = {command: res}
+
             return VIRT_SUCCESS, res
 
         elif hasattr(v, command):


Test patch: