diff --git a/lib/ansible/modules/cloud/xenserver/xenserver_guest_facts.py b/lib/ansible/modules/cloud/xenserver/xenserver_guest_facts.py new file mode 100644 index 00000000000..28341796901 --- /dev/null +++ b/lib/ansible/modules/cloud/xenserver/xenserver_guest_facts.py @@ -0,0 +1,218 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, Bojan Vitnik +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: xenserver_guest_facts +short_description: Gathers facts for virtual machines running on Citrix XenServer host or pool +description: > + This module can be used to gather essential VM facts. +version_added: '2.8' +author: +- Bojan Vitnik (@bvitnik) +notes: +- Minimal supported version of XenServer is 5.6 +- Module was tested with XenServer 6.5, 7.1 and 7.2 +- 'If no scheme is specified in C(hostname), module defaults to C(http://) because C(https://) is problematic in most setups. Make sure you are + accessing XenServer host in trusted environment or use C(https://) scheme explicitly.' +- 'To use C(https://) scheme for C(hostname) you have to either import host certificate to your OS certificate store or use C(validate_certs: no) + which requires XenAPI.py from XenServer 7.2 SDK or newer and Python 2.7.9 or newer.' +requirements: +- python >= 2.6 +- XenAPI +options: + name: + description: + - Name of the VM to gather fact. + - VMs running on XenServer do not necessarily have unique names. The module will fail if multiple VMs with same name are found. + - In case of multiple VMs with same name, use C(uuid) to uniquely specify VM to manage. + - This parameter is case sensitive. + required: yes + aliases: [ 'name_label' ] + uuid: + description: + - UUID of the VM to gather fact of, this is XenServer's unique identifier. + - It is required if name is not unique. +extends_documentation_fragment: xenserver.documentation +''' + +EXAMPLES = r''' +- name: Gather facts + xenserver_guest_facts: + hostname: 192.168.1.209 + username: root + password: xenserver + name: testvm_11 + delegate_to: localhost + register: facts +''' + +RETURN = r''' +instance: + description: Metadata about the VM + returned: always + type: dict + sample: { + "cdrom": { + "type": "none" + }, + "customization_agent": "native", + "disks": [ + { + "name": "testvm_11-0", + "name_desc": "", + "os_device": "xvda", + "size": 42949672960, + "sr": "Local storage", + "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075", + "vbd_userdevice": "0" + }, + { + "name": "testvm_11-1", + "name_desc": "", + "os_device": "xvdb", + "size": 42949672960, + "sr": "Local storage", + "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075", + "vbd_userdevice": "1" + } + ], + "domid": "56", + "folder": "", + "hardware": { + "memory_mb": 8192, + "num_cpu_cores_per_socket": 2, + "num_cpus": 4 + }, + "home_server": "", + "is_template": false, + "name": "testvm_11", + "name_desc": "", + "networks": [ + { + "gateway": "192.168.0.254", + "gateway6": "fc00::fffe", + "ip": "192.168.0.200", + "ip6": [ + "fe80:0000:0000:0000:e9cb:625a:32c5:c291", + "fc00:0000:0000:0000:0000:0000:0000:0001" + ], + "mac": "ba:91:3a:48:20:76", + "mtu": "1500", + "name": "Pool-wide network associated with eth1", + "netmask": "255.255.255.128", + "prefix": "25", + "prefix6": "64", + "vif_device": "0" + } + ], + "other_config": { + "base_template_name": "Windows Server 2016 (64-bit)", + "import_task": "OpaqueRef:e43eb71c-45d6-5351-09ff-96e4fb7d0fa5", + "install-methods": "cdrom", + "instant": "true", + "mac_seed": "f83e8d8a-cfdc-b105-b054-ef5cb416b77e" + }, + "platform": { + "acpi": "1", + "apic": "true", + "cores-per-socket": "2", + "device_id": "0002", + "hpet": "true", + "nx": "true", + "pae": "true", + "timeoffset": "-25200", + "vga": "std", + "videoram": "8", + "viridian": "true", + "viridian_reference_tsc": "true", + "viridian_time_ref_count": "true" + }, + "state": "poweredon", + "uuid": "e3c0b2d5-5f05-424e-479c-d3df8b3e7cda", + "xenstore_data": { + "vm-data": "" + } + } +''' + +HAS_XENAPI = False +try: + import XenAPI + HAS_XENAPI = True +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.xenserver import (xenserver_common_argument_spec, XAPI, XenServerObject, get_object_ref, + gather_vm_params, gather_vm_facts) + + +class XenServerVM(XenServerObject): + """Class for managing XenServer VM. + + Attributes: + vm_ref (str): XAPI reference to VM. + vm_params (dict): A dictionary with VM parameters as returned + by gather_vm_params() function. + """ + + def __init__(self, module): + """Inits XenServerVM using module parameters. + + Args: + module: Reference to AnsibleModule object. + """ + super(XenServerVM, self).__init__(module) + + self.vm_ref = get_object_ref(self.module, self.module.params['name'], self.module.params['uuid'], obj_type="VM", fail=True, msg_prefix="VM search: ") + self.gather_params() + + def gather_params(self): + """Gathers all VM parameters available in XAPI database.""" + self.vm_params = gather_vm_params(self.module, self.vm_ref) + + def gather_facts(self): + """Gathers and returns VM facts.""" + return gather_vm_facts(self.module, self.vm_params) + + +def main(): + argument_spec = xenserver_common_argument_spec() + argument_spec.update( + name=dict(type='str', aliases=['name_label']), + uuid=dict(type='str'), + ) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=[ + ['name', 'uuid'], + ], + ) + + result = {'failed': False, 'changed': False} + + vm = XenServerVM(module) + + # Gather facts. + result['instance'] = vm.gather_facts() + + if result['failed']: + module.fail_json(**result) + else: + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/cloud/xenserver/FakeXenAPI.py b/test/units/modules/cloud/xenserver/FakeXenAPI.py new file mode 100644 index 00000000000..dc657a6a090 --- /dev/null +++ b/test/units/modules/cloud/xenserver/FakeXenAPI.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, Bojan Vitnik +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +FAKE_API_VERSION = "1.1" + + +class Failure(Exception): + def __init__(self, details): + self.details = details + + def __str__(self): + return str(self.details) + + +class Session(object): + def __init__(self, uri, transport=None, encoding=None, verbose=0, + allow_none=1, ignore_ssl=False): + + self.transport = transport + self._session = None + self.last_login_method = None + self.last_login_params = None + self.API_version = FAKE_API_VERSION + + def _get_api_version(self): + return FAKE_API_VERSION + + def _login(self, method, params): + self._session = "OpaqueRef:fake-xenapi-session-ref" + self.last_login_method = method + self.last_login_params = params + self.API_version = self._get_api_version() + + def _logout(self): + self._session = None + self.last_login_method = None + self.last_login_params = None + self.API_version = FAKE_API_VERSION + + def xenapi_request(self, methodname, params): + if methodname.startswith('login'): + self._login(methodname, params) + return None + elif methodname == 'logout' or methodname == 'session.logout': + self._logout() + return None + else: + # Should be patched with mocker.patch(). + return None + + def __getattr__(self, name): + if name == 'handle': + return self._session + elif name == 'xenapi': + # Should be patched with mocker.patch(). + return None + elif name.startswith('login') or name.startswith('slave_local'): + return lambda *params: self._login(name, params) + elif name == 'logout': + return self._logout + + +def xapi_local(): + return Session("http://_var_lib_xcp_xapi/") diff --git a/test/units/modules/cloud/xenserver/__init__.py b/test/units/modules/cloud/xenserver/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/units/modules/cloud/xenserver/common.py b/test/units/modules/cloud/xenserver/common.py new file mode 100644 index 00000000000..6e652a5d714 --- /dev/null +++ b/test/units/modules/cloud/xenserver/common.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, Bojan Vitnik +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +def fake_xenapi_ref(xenapi_class): + return "OpaqueRef:fake-xenapi-%s-ref" % xenapi_class diff --git a/test/units/modules/cloud/xenserver/conftest.py b/test/units/modules/cloud/xenserver/conftest.py new file mode 100644 index 00000000000..4b4d1e7c597 --- /dev/null +++ b/test/units/modules/cloud/xenserver/conftest.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, Bojan Vitnik +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import sys +import importlib +import pytest + + +@pytest.fixture(autouse=True) +def XenAPI(): + """Imports and returns fake XenAPI module.""" + + # Import of fake XenAPI module is wrapped by fixture so that it does not + # affect other unit tests which could potentialy also use XenAPI module. + + # First we use importlib.import_module() to import the module and assign + # it to a local symbol. + fake_xenapi = importlib.import_module('units.module_utils.xenserver.FakeXenAPI') + + # Now we populate Python module cache with imported fake module using the + # original module name (XenAPI). That way, any 'import XenAPI' statement + # will just load already imported fake module from the cache. + sys.modules['XenAPI'] = fake_xenapi + + return fake_xenapi + + +@pytest.fixture +def xenserver_guest_facts(XenAPI): + """Imports and returns xenserver_guest_facts module.""" + + # Since we are wrapping fake XenAPI module inside a fixture, all modules + # that depend on it have to be imported inside a test function. To make + # this easier to handle and remove some code repetition, we wrap the import + # of xenserver_guest_facts module with a fixture. + from ansible.modules.cloud.xenserver import xenserver_guest_facts + + return xenserver_guest_facts diff --git a/test/units/modules/cloud/xenserver/test_xenserver_guest_facts.py b/test/units/modules/cloud/xenserver/test_xenserver_guest_facts.py new file mode 100644 index 00000000000..2208849e5cc --- /dev/null +++ b/test/units/modules/cloud/xenserver/test_xenserver_guest_facts.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, Bojan Vitnik +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import json +import pytest + +from .common import fake_xenapi_ref + +pytestmark = pytest.mark.usefixtures('patch_ansible_module') + + +testcase_module_params = { + "params": [ + { + "hostname": "somehost", + "username": "someuser", + "password": "somepwd", + "name": "somevmname", + }, + { + "hostname": "somehost", + "username": "someuser", + "password": "somepwd", + "uuid": "somevmuuid", + }, + { + "hostname": "somehost", + "username": "someuser", + "password": "somepwd", + "name": "somevmname", + "uuid": "somevmuuid", + }, + ], + "ids": [ + "name", + "uuid", + "name+uuid", + ], +} + + +@pytest.mark.parametrize('patch_ansible_module', testcase_module_params['params'], ids=testcase_module_params['ids'], indirect=True) +def test_xenserver_guest_facts(mocker, capfd, XenAPI, xenserver_guest_facts): + """ + Tests regular module invocation including parsing and propagation of + module params and module output. + """ + fake_vm_facts = {"fake-vm-fact": True} + + mocker.patch('ansible.modules.cloud.xenserver.xenserver_guest_facts.get_object_ref', return_value=None) + mocker.patch('ansible.modules.cloud.xenserver.xenserver_guest_facts.gather_vm_params', return_value=None) + mocker.patch('ansible.modules.cloud.xenserver.xenserver_guest_facts.gather_vm_facts', return_value=fake_vm_facts) + + mocked_xenapi = mocker.patch.object(XenAPI.Session, 'xenapi', create=True) + + mocked_returns = { + "pool.get_all.return_value": [fake_xenapi_ref('pool')], + "pool.get_default_SR.return_value": fake_xenapi_ref('SR'), + } + + mocked_xenapi.configure_mock(**mocked_returns) + + mocker.patch('ansible.module_utils.xenserver.get_xenserver_version', return_value=[7, 2, 0]) + + with pytest.raises(SystemExit): + xenserver_guest_facts.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert result['instance'] == fake_vm_facts