# -*- coding: utf-8 -*- # Copyright (c) 2021 Ansible Project # 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 = ''' name: xen_orchestra short_description: Xen Orchestra inventory source version_added: 4.1.0 author: - Dom Del Nano (@ddelnano) - Samori Gorse (@shinuza) requirements: - websocket-client >= 1.0.0 description: - Get inventory hosts from a Xen Orchestra deployment. - 'Uses a configuration file as an inventory source, it must end in C(.xen_orchestra.yml) or C(.xen_orchestra.yaml).' extends_documentation_fragment: - constructed - inventory_cache options: plugin: description: The name of this plugin, it should always be set to V(community.general.xen_orchestra) for this plugin to recognize it as its own. required: true choices: ['community.general.xen_orchestra'] type: str api_host: description: - API host to XOA API. - If the value is not specified in the inventory configuration, the value of environment variable E(ANSIBLE_XO_HOST) will be used instead. type: str env: - name: ANSIBLE_XO_HOST user: description: - Xen Orchestra user. - If the value is not specified in the inventory configuration, the value of environment variable E(ANSIBLE_XO_USER) will be used instead. required: true type: str env: - name: ANSIBLE_XO_USER password: description: - Xen Orchestra password. - If the value is not specified in the inventory configuration, the value of environment variable E(ANSIBLE_XO_PASSWORD) will be used instead. required: true type: str env: - name: ANSIBLE_XO_PASSWORD validate_certs: description: Verify TLS certificate if using HTTPS. type: boolean default: true use_ssl: description: Use wss when connecting to the Xen Orchestra API type: boolean default: true ''' EXAMPLES = ''' # file must be named xen_orchestra.yaml or xen_orchestra.yml plugin: community.general.xen_orchestra api_host: 192.168.1.255 user: xo password: xo_pwd validate_certs: true use_ssl: true groups: kube_nodes: "'kube_node' in tags" compose: ansible_port: 2222 ''' import json import ssl from time import sleep from ansible.errors import AnsibleError from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable from ansible_collections.community.general.plugins.module_utils.version import LooseVersion from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe # 3rd party imports try: HAS_WEBSOCKET = True import websocket from websocket import create_connection if LooseVersion(websocket.__version__) <= LooseVersion('1.0.0'): raise ImportError except ImportError as e: HAS_WEBSOCKET = False HALTED = 'Halted' PAUSED = 'Paused' RUNNING = 'Running' SUSPENDED = 'Suspended' POWER_STATES = [RUNNING, HALTED, SUSPENDED, PAUSED] HOST_GROUP = 'xo_hosts' POOL_GROUP = 'xo_pools' def clean_group_name(label): return label.lower().replace(' ', '-').replace('-', '_') class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): ''' Host inventory parser for ansible using XenOrchestra as source. ''' NAME = 'community.general.xen_orchestra' def __init__(self): super(InventoryModule, self).__init__() # from config self.counter = -1 self.session = None self.cache_key = None self.use_cache = None @property def pointer(self): self.counter += 1 return self.counter def create_connection(self, xoa_api_host): validate_certs = self.get_option('validate_certs') use_ssl = self.get_option('use_ssl') proto = 'wss' if use_ssl else 'ws' sslopt = None if validate_certs else {'cert_reqs': ssl.CERT_NONE} self.conn = create_connection( '{0}://{1}/api/'.format(proto, xoa_api_host), sslopt=sslopt) CALL_TIMEOUT = 100 """Number of 1/10ths of a second to wait before method call times out.""" def call(self, method, params): """Calls a method on the XO server with the provided parameters.""" id = self.pointer self.conn.send(json.dumps({ 'id': id, 'jsonrpc': '2.0', 'method': method, 'params': params })) waited = 0 while waited < self.CALL_TIMEOUT: response = json.loads(self.conn.recv()) if 'id' in response and response['id'] == id: return response else: sleep(0.1) waited += 1 raise AnsibleError( 'Method call {method} timed out after {timeout} seconds.'.format(method=method, timeout=self.CALL_TIMEOUT / 10)) def login(self, user, password): result = self.call('session.signIn', { 'username': user, 'password': password }) if 'error' in result: raise AnsibleError( 'Could not connect: {0}'.format(result['error'])) def get_object(self, name): answer = self.call('xo.getAllObjects', {'filter': {'type': name}}) if 'error' in answer: raise AnsibleError( 'Could not request: {0}'.format(answer['error'])) return answer['result'] def _get_objects(self): self.create_connection(self.xoa_api_host) self.login(self.xoa_user, self.xoa_password) return { 'vms': self.get_object('VM'), 'pools': self.get_object('pool'), 'hosts': self.get_object('host'), } def _apply_constructable(self, name, variables): strict = self.get_option('strict') self._add_host_to_composed_groups(self.get_option('groups'), variables, name, strict=strict) self._add_host_to_keyed_groups(self.get_option('keyed_groups'), variables, name, strict=strict) self._set_composite_vars(self.get_option('compose'), variables, name, strict=strict) def _add_vms(self, vms, hosts, pools): for uuid, vm in vms.items(): group = 'with_ip' ip = vm.get('mainIpAddress') entry_name = uuid power_state = vm['power_state'].lower() pool_name = self._pool_group_name_for_uuid(pools, vm['$poolId']) host_name = self._host_group_name_for_uuid(hosts, vm['$container']) self.inventory.add_host(entry_name) # Grouping by power state self.inventory.add_child(power_state, entry_name) # Grouping by host if host_name: self.inventory.add_child(host_name, entry_name) # Grouping by pool if pool_name: self.inventory.add_child(pool_name, entry_name) # Grouping VMs with an IP together if ip is None: group = 'without_ip' self.inventory.add_group(group) self.inventory.add_child(group, entry_name) # Adding meta self.inventory.set_variable(entry_name, 'uuid', uuid) self.inventory.set_variable(entry_name, 'ip', ip) self.inventory.set_variable(entry_name, 'ansible_host', ip) self.inventory.set_variable(entry_name, 'power_state', power_state) self.inventory.set_variable( entry_name, 'name_label', vm['name_label']) self.inventory.set_variable(entry_name, 'type', vm['type']) self.inventory.set_variable( entry_name, 'cpus', vm['CPUs']['number']) self.inventory.set_variable(entry_name, 'tags', vm['tags']) self.inventory.set_variable( entry_name, 'memory', vm['memory']['size']) self.inventory.set_variable( entry_name, 'has_ip', group == 'with_ip') self.inventory.set_variable( entry_name, 'is_managed', vm.get('managementAgentDetected', False)) self.inventory.set_variable( entry_name, 'os_version', vm['os_version']) self._apply_constructable(entry_name, self.inventory.get_host(entry_name).get_vars()) def _add_hosts(self, hosts, pools): for host in hosts.values(): entry_name = host['uuid'] group_name = 'xo_host_{0}'.format( clean_group_name(host['name_label'])) pool_name = self._pool_group_name_for_uuid(pools, host['$poolId']) self.inventory.add_group(group_name) self.inventory.add_host(entry_name) self.inventory.add_child(HOST_GROUP, entry_name) self.inventory.add_child(pool_name, entry_name) self.inventory.set_variable(entry_name, 'enabled', host['enabled']) self.inventory.set_variable( entry_name, 'hostname', host['hostname']) self.inventory.set_variable(entry_name, 'memory', host['memory']) self.inventory.set_variable(entry_name, 'address', host['address']) self.inventory.set_variable(entry_name, 'cpus', host['cpus']) self.inventory.set_variable(entry_name, 'type', 'host') self.inventory.set_variable(entry_name, 'tags', host['tags']) self.inventory.set_variable(entry_name, 'version', host['version']) self.inventory.set_variable( entry_name, 'power_state', host['power_state'].lower()) self.inventory.set_variable( entry_name, 'product_brand', host['productBrand']) for pool in pools.values(): group_name = 'xo_pool_{0}'.format( clean_group_name(pool['name_label'])) self.inventory.add_group(group_name) def _add_pools(self, pools): for pool in pools.values(): group_name = 'xo_pool_{0}'.format( clean_group_name(pool['name_label'])) self.inventory.add_group(group_name) # TODO: Refactor def _pool_group_name_for_uuid(self, pools, pool_uuid): for pool in pools: if pool == pool_uuid: return 'xo_pool_{0}'.format( clean_group_name(pools[pool_uuid]['name_label'])) # TODO: Refactor def _host_group_name_for_uuid(self, hosts, host_uuid): for host in hosts: if host == host_uuid: return 'xo_host_{0}'.format( clean_group_name(hosts[host_uuid]['name_label'] )) def _populate(self, objects): # Prepare general groups self.inventory.add_group(HOST_GROUP) self.inventory.add_group(POOL_GROUP) for group in POWER_STATES: self.inventory.add_group(group.lower()) self._add_pools(objects['pools']) self._add_hosts(objects['hosts'], objects['pools']) self._add_vms(objects['vms'], objects['hosts'], objects['pools']) def verify_file(self, path): valid = False if super(InventoryModule, self).verify_file(path): if path.endswith(('xen_orchestra.yaml', 'xen_orchestra.yml')): valid = True else: self.display.vvv( 'Skipping due to inventory source not ending in "xen_orchestra.yaml" nor "xen_orchestra.yml"') return valid def parse(self, inventory, loader, path, cache=True): if not HAS_WEBSOCKET: raise AnsibleError('This plugin requires websocket-client 1.0.0 or higher: ' 'https://github.com/websocket-client/websocket-client.') super(InventoryModule, self).parse(inventory, loader, path) # read config from file, this sets 'options' self._read_config_data(path) self.inventory = inventory self.protocol = 'wss' self.xoa_api_host = self.get_option('api_host') self.xoa_user = self.get_option('user') self.xoa_password = self.get_option('password') self.cache_key = self.get_cache_key(path) self.use_cache = cache and self.get_option('cache') self.validate_certs = self.get_option('validate_certs') if not self.get_option('use_ssl'): self.protocol = 'ws' objects = self._get_objects() self._populate(make_unsafe(objects))