#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2017, Milan Ilic # Copyright (c) 2019, Jan Meerkamp # 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 # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' --- module: one_vm short_description: Creates or terminates OpenNebula instances description: - Manages OpenNebula instances requirements: - pyone extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: full diff_mode: support: none options: api_url: description: - URL of the OpenNebula RPC server. - It is recommended to use HTTPS so that the username/password are not transferred over the network unencrypted. - If not set then the value of the E(ONE_URL) environment variable is used. type: str api_username: description: - Name of the user to login into the OpenNebula RPC server. If not set then the value of the E(ONE_USERNAME) environment variable is used. type: str api_password: description: - Password of the user to login into OpenNebula RPC server. If not set then the value of the E(ONE_PASSWORD) environment variable is used. if both O(api_username) or O(api_password) are not set, then it will try authenticate with ONE auth file. Default path is "~/.one/one_auth". - Set environment variable E(ONE_AUTH) to override this path. type: str template_name: description: - Name of VM template to use to create a new instance type: str template_id: description: - ID of a VM template to use to create a new instance type: int vm_start_on_hold: description: - Set to true to put vm on hold while creating default: false type: bool instance_ids: description: - 'A list of instance ids used for states: V(absent), V(running), V(rebooted), V(poweredoff).' aliases: ['ids'] type: list elements: int state: description: - V(present) - create instances from a template specified with C(template_id)/C(template_name). - V(running) - run instances - V(poweredoff) - power-off instances - V(rebooted) - reboot instances - V(absent) - terminate instances choices: ["present", "absent", "running", "rebooted", "poweredoff"] default: present type: str hard: description: - Reboot, power-off or terminate instances C(hard). default: false type: bool wait: description: - Wait for the instance to reach its desired state before returning. Keep in mind if you are waiting for instance to be in running state it doesn't mean that you will be able to SSH on that machine only that boot process have started on that instance, see 'wait_for' example for details. default: true type: bool wait_timeout: description: - How long before wait gives up, in seconds default: 300 type: int attributes: description: - A dictionary of key/value attributes to add to new instances, or for setting C(state) of instances with these attributes. - Keys are case insensitive and OpenNebula automatically converts them to upper case. - Be aware C(NAME) is a special attribute which sets the name of the VM when it's deployed. - C(#) character(s) can be appended to the C(NAME) and the module will automatically add indexes to the names of VMs. - For example':' C(NAME':' foo-###) would create VMs with names C(foo-000), C(foo-001),... - When used with O(count_attributes) and O(exact_count) the module will match the base name without the index part. default: {} type: dict labels: description: - A list of labels to associate with new instances, or for setting C(state) of instances with these labels. default: [] type: list elements: str count_attributes: description: - A dictionary of key/value attributes that can only be used with O(exact_count) to determine how many nodes based on a specific attributes criteria should be deployed. This can be expressed in multiple ways and is shown in the EXAMPLES section. type: dict count_labels: description: - A list of labels that can only be used with O(exact_count) to determine how many nodes based on a specific labels criteria should be deployed. This can be expressed in multiple ways and is shown in the EXAMPLES section. type: list elements: str count: description: - Number of instances to launch default: 1 type: int exact_count: description: - Indicates how many instances that match O(count_attributes) and O(count_labels) parameters should be deployed. Instances are either created or terminated based on this value. - 'B(NOTE:) Instances with the least IDs will be terminated first.' type: int mode: description: - Set permission mode of the instance in octet format, for example V(0600) to give owner C(use) and C(manage) and nothing to group and others. type: str owner_id: description: - ID of the user which will be set as the owner of the instance type: int group_id: description: - ID of the group which will be set as the group of the instance type: int memory: description: - The size of the memory for new instances (in MB, GB, ...) type: str disk_size: description: - The size of the disk created for new instances (in MB, GB, TB,...). - 'B(NOTE:) If The Template hats Multiple Disks the Order of the Sizes is matched against the order specified in O(template_id)/O(template_name).' type: list elements: str cpu: description: - Percentage of CPU divided by 100 required for the new instance. Half a processor is written 0.5. type: float vcpu: description: - Number of CPUs (cores) new VM will have. type: int networks: description: - A list of dictionaries with network parameters. See examples for more details. default: [] type: list elements: dict disk_saveas: description: - Creates an image from a VM disk. - It is a dictionary where you have to specify C(name) of the new image. - Optionally you can specify C(disk_id) of the disk you want to save. By default C(disk_id) is 0. - 'B(NOTE:) This operation will only be performed on the first VM (if more than one VM ID is passed) and the VM has to be in the C(poweredoff) state.' - Also this operation will fail if an image with specified C(name) already exists. type: dict persistent: description: - Create a private persistent copy of the template plus any image defined in DISK, and instantiate that copy. default: false type: bool version_added: '0.2.0' datastore_id: description: - Name of Datastore to use to create a new instance version_added: '0.2.0' type: int datastore_name: description: - Name of Datastore to use to create a new instance version_added: '0.2.0' type: str updateconf: description: - When O(instance_ids) is provided, updates running VMs with the C(updateconf) API call. - When new VMs are being created, emulates the C(updateconf) API call via direct template merge. - Allows for complete modifications of the C(CONTEXT) attribute. type: dict version_added: 6.3.0 author: - "Milan Ilic (@ilicmilan)" - "Jan Meerkamp (@meerkampdvv)" ''' EXAMPLES = ''' - name: Create a new instance community.general.one_vm: template_id: 90 register: result - name: Print VM properties ansible.builtin.debug: msg: result - name: Deploy a new VM on hold community.general.one_vm: template_name: 'app1_template' vm_start_on_hold: 'True' - name: Deploy a new VM and set its name to 'foo' community.general.one_vm: template_name: 'app1_template' attributes: name: foo - name: Deploy a new VM and set its group_id and mode community.general.one_vm: template_id: 90 group_id: 16 mode: 660 - name: Deploy a new VM as persistent community.general.one_vm: template_id: 90 persistent: true - name: Change VM's permissions to 640 community.general.one_vm: instance_ids: 5 mode: 640 - name: Deploy 2 new instances and set memory, vcpu, disk_size and 3 networks community.general.one_vm: template_id: 15 disk_size: 35.2 GB memory: 4 GB vcpu: 4 count: 2 networks: - NETWORK_ID: 27 - NETWORK: "default-network" NETWORK_UNAME: "app-user" SECURITY_GROUPS: "120,124" - NETWORK_ID: 27 SECURITY_GROUPS: "10" - name: Deploy a new instance which uses a Template with two Disks community.general.one_vm: template_id: 42 disk_size: - 35.2 GB - 50 GB memory: 4 GB vcpu: 4 count: 1 networks: - NETWORK_ID: 27 - name: "Deploy an new instance with attribute 'bar: bar1' and set its name to 'foo'" community.general.one_vm: template_id: 53 attributes: name: foo bar: bar1 - name: "Enforce that 2 instances with attributes 'foo1: app1' and 'foo2: app2' are deployed" community.general.one_vm: template_id: 53 attributes: foo1: app1 foo2: app2 exact_count: 2 count_attributes: foo1: app1 foo2: app2 - name: Enforce that 4 instances with an attribute 'bar' are deployed community.general.one_vm: template_id: 53 attributes: name: app bar: bar2 exact_count: 4 count_attributes: bar: # Deploy 2 new instances with attribute 'foo: bar' and labels 'app1' and 'app2' and names in format 'fooapp-##' # Names will be: fooapp-00 and fooapp-01 - name: Deploy 2 new instances community.general.one_vm: template_id: 53 attributes: name: fooapp-## foo: bar labels: - app1 - app2 count: 2 # Deploy 2 new instances with attribute 'app: app1' and names in format 'fooapp-###' # Names will be: fooapp-002 and fooapp-003 - name: Deploy 2 new instances community.general.one_vm: template_id: 53 attributes: name: fooapp-### app: app1 count: 2 # Reboot all instances with name in format 'fooapp-#' # Instances 'fooapp-00', 'fooapp-01', 'fooapp-002' and 'fooapp-003' will be rebooted - name: Reboot all instances with names in a certain format community.general.one_vm: attributes: name: fooapp-# state: rebooted # Enforce that only 1 instance with name in format 'fooapp-#' is deployed # The task will delete oldest instances, so only the 'fooapp-003' will remain - name: Enforce that only 1 instance with name in a certain format is deployed community.general.one_vm: template_id: 53 exact_count: 1 count_attributes: name: fooapp-# - name: Deploy an new instance with a network community.general.one_vm: template_id: 53 networks: - NETWORK_ID: 27 register: vm - name: Wait for SSH to come up ansible.builtin.wait_for_connection: delegate_to: '{{ vm.instances[0].networks[0].ip }}' - name: Terminate VMs by ids community.general.one_vm: instance_ids: - 153 - 160 state: absent - name: Reboot all VMs that have labels 'foo' and 'app1' community.general.one_vm: labels: - foo - app1 state: rebooted - name: "Fetch all VMs that have name 'foo' and attribute 'app: bar'" community.general.one_vm: attributes: name: foo app: bar register: results - name: Deploy 2 new instances with labels 'foo1' and 'foo2' community.general.one_vm: template_name: app_template labels: - foo1 - foo2 count: 2 - name: Enforce that only 1 instance with label 'foo1' will be running community.general.one_vm: template_name: app_template labels: - foo1 exact_count: 1 count_labels: - foo1 - name: Terminate all instances that have attribute foo community.general.one_vm: template_id: 53 exact_count: 0 count_attributes: foo: - name: "Power-off the VM and save VM's disk with id=0 to the image with name 'foo-image'" community.general.one_vm: instance_ids: 351 state: poweredoff disk_saveas: name: foo-image - name: "Save VM's disk with id=1 to the image with name 'bar-image'" community.general.one_vm: instance_ids: 351 disk_saveas: name: bar-image disk_id: 1 - name: "Deploy 2 new instances with a custom 'start script'" community.general.one_vm: template_name: app_template count: 2 updateconf: CONTEXT: START_SCRIPT: ip r r 169.254.16.86/32 dev eth0 - name: "Add a custom 'start script' to a running VM" community.general.one_vm: instance_ids: 351 updateconf: CONTEXT: START_SCRIPT: ip r r 169.254.16.86/32 dev eth0 - name: "Update SSH public keys inside the VM's context" community.general.one_vm: instance_ids: 351 updateconf: CONTEXT: SSH_PUBLIC_KEY: |- ssh-rsa ... ssh-ed25519 ... ''' RETURN = ''' instances_ids: description: a list of instances ids whose state is changed or which are fetched with O(instance_ids) option. type: list returned: success sample: [ 1234, 1235 ] instances: description: a list of instances info whose state is changed or which are fetched with O(instance_ids) option. type: complex returned: success contains: vm_id: description: vm id type: int sample: 153 vm_name: description: vm name type: str sample: foo template_id: description: vm's template id type: int sample: 153 group_id: description: vm's group id type: int sample: 1 group_name: description: vm's group name type: str sample: one-users owner_id: description: vm's owner id type: int sample: 143 owner_name: description: vm's owner name type: str sample: app-user mode: description: vm's mode type: str returned: success sample: 660 state: description: state of an instance type: str sample: ACTIVE lcm_state: description: lcm state of an instance that is only relevant when the state is ACTIVE type: str sample: RUNNING cpu: description: Percentage of CPU divided by 100 type: float sample: 0.2 vcpu: description: Number of CPUs (cores) type: int sample: 2 memory: description: The size of the memory in MB type: str sample: 4096 MB disk_size: description: The size of the disk in MB type: str sample: 20480 MB networks: description: a list of dictionaries with info about IP, NAME, MAC, SECURITY_GROUPS for each NIC type: list sample: [ { "ip": "10.120.5.33", "mac": "02:00:0a:78:05:21", "name": "default-test-private", "security_groups": "0,10" }, { "ip": "10.120.5.34", "mac": "02:00:0a:78:05:22", "name": "default-test-private", "security_groups": "0" } ] uptime_h: description: Uptime of the instance in hours type: int sample: 35 labels: description: A list of string labels that are associated with the instance type: list sample: [ "foo", "spec-label" ] attributes: description: A dictionary of key/values attributes that are associated with the instance type: dict sample: { "HYPERVISOR": "kvm", "LOGO": "images/logos/centos.png", "TE_GALAXY": "bar", "USER_INPUTS": null } updateconf: description: A dictionary of key/values attributes that are set with the updateconf API call. type: dict version_added: 6.3.0 sample: { "OS": { "ARCH": "x86_64" }, "CONTEXT": { "START_SCRIPT": "ip r r 169.254.16.86/32 dev eth0", "SSH_PUBLIC_KEY": "ssh-rsa ...\\nssh-ed25519 ..." } } tagged_instances: description: - A list of instances info based on a specific attributes and/or - labels that are specified with O(count_attributes) and O(count_labels) - options. type: complex returned: success contains: vm_id: description: vm id type: int sample: 153 vm_name: description: vm name type: str sample: foo template_id: description: vm's template id type: int sample: 153 group_id: description: vm's group id type: int sample: 1 group_name: description: vm's group name type: str sample: one-users owner_id: description: vm's user id type: int sample: 143 owner_name: description: vm's user name type: str sample: app-user mode: description: vm's mode type: str returned: success sample: 660 state: description: state of an instance type: str sample: ACTIVE lcm_state: description: lcm state of an instance that is only relevant when the state is ACTIVE type: str sample: RUNNING cpu: description: Percentage of CPU divided by 100 type: float sample: 0.2 vcpu: description: Number of CPUs (cores) type: int sample: 2 memory: description: The size of the memory in MB type: str sample: 4096 MB disk_size: description: The size of the disk in MB type: list sample: [ "20480 MB", "10240 MB" ] networks: description: a list of dictionaries with info about IP, NAME, MAC, SECURITY_GROUPS for each NIC type: list sample: [ { "ip": "10.120.5.33", "mac": "02:00:0a:78:05:21", "name": "default-test-private", "security_groups": "0,10" }, { "ip": "10.120.5.34", "mac": "02:00:0a:78:05:22", "name": "default-test-private", "security_groups": "0" } ] uptime_h: description: Uptime of the instance in hours type: int sample: 35 labels: description: A list of string labels that are associated with the instance type: list sample: [ "foo", "spec-label" ] attributes: description: A dictionary of key/values attributes that are associated with the instance type: dict sample: { "HYPERVISOR": "kvm", "LOGO": "images/logos/centos.png", "TE_GALAXY": "bar", "USER_INPUTS": null } updateconf: description: A dictionary of key/values attributes that are set with the updateconf API call type: dict version_added: 6.3.0 sample: { "OS": { "ARCH": "x86_64" }, "CONTEXT": { "START_SCRIPT": "ip r r 169.254.16.86/32 dev eth0", "SSH_PUBLIC_KEY": "ssh-rsa ...\\nssh-ed25519 ..." } } ''' try: import pyone HAS_PYONE = True except ImportError: HAS_PYONE = False import os from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.dict_transformations import dict_merge from ansible_collections.community.general.plugins.module_utils.opennebula import flatten, render UPDATECONF_ATTRIBUTES = { "OS": ["ARCH", "MACHINE", "KERNEL", "INITRD", "BOOTLOADER", "BOOT", "SD_DISK_BUS", "UUID"], "FEATURES": ["ACPI", "PAE", "APIC", "LOCALTIME", "HYPERV", "GUEST_AGENT"], "INPUT": ["TYPE", "BUS"], "GRAPHICS": ["TYPE", "LISTEN", "PASSWD", "KEYMAP"], "RAW": ["DATA", "DATA_VMX", "TYPE"], "CONTEXT": [], } def check_updateconf(module, to_check): '''Checks if attributes are compatible with one.vm.updateconf API call.''' for attr, subattributes in to_check.items(): if attr not in UPDATECONF_ATTRIBUTES: module.fail_json(msg="'{0:}' is not a valid VM attribute.".format(attr)) if not UPDATECONF_ATTRIBUTES[attr]: continue for subattr in subattributes: if subattr not in UPDATECONF_ATTRIBUTES[attr]: module.fail_json(msg="'{0:}' is not a valid VM subattribute of '{1:}'".format(subattr, attr)) def parse_updateconf(vm_template): '''Extracts 'updateconf' attributes from a VM template.''' updateconf = {} for attr, subattributes in vm_template.items(): if attr not in UPDATECONF_ATTRIBUTES: continue tmp = {} for subattr, value in subattributes.items(): if UPDATECONF_ATTRIBUTES[attr] and subattr not in UPDATECONF_ATTRIBUTES[attr]: continue tmp[subattr] = value if tmp: updateconf[attr] = tmp return updateconf def get_template(module, client, predicate): pool = client.templatepool.info(-2, -1, -1, -1) # Filter -2 means fetch all templates user can Use found = 0 found_template = None template_name = '' for template in pool.VMTEMPLATE: if predicate(template): found = found + 1 found_template = template template_name = template.NAME if found == 0: return None elif found > 1: module.fail_json(msg='There are more templates with name: ' + template_name) return found_template def get_template_by_name(module, client, template_name): return get_template(module, client, lambda template: (template.NAME == template_name)) def get_template_by_id(module, client, template_id): return get_template(module, client, lambda template: (template.ID == template_id)) def get_template_id(module, client, requested_id, requested_name): template = get_template_by_id(module, client, requested_id) if requested_id is not None else get_template_by_name(module, client, requested_name) if template: return template.ID else: return None def get_datastore(module, client, predicate): pool = client.datastorepool.info() found = 0 found_datastore = None datastore_name = '' for datastore in pool.DATASTORE: if predicate(datastore): found = found + 1 found_datastore = datastore datastore_name = datastore.NAME if found == 0: return None elif found > 1: module.fail_json(msg='There are more datastores with name: ' + datastore_name) return found_datastore def get_datastore_by_name(module, client, datastore_name): return get_datastore(module, client, lambda datastore: (datastore.NAME == datastore_name)) def get_datastore_by_id(module, client, datastore_id): return get_datastore(module, client, lambda datastore: (datastore.ID == datastore_id)) def get_datastore_id(module, client, requested_id, requested_name): datastore = get_datastore_by_id(module, client, requested_id) if requested_id else get_datastore_by_name(module, client, requested_name) if datastore: return datastore.ID else: return None def get_vm_by_id(client, vm_id): try: vm = client.vm.info(int(vm_id)) except BaseException: return None return vm def get_vms_by_ids(module, client, state, ids): vms = [] for vm_id in ids: vm = get_vm_by_id(client, vm_id) if vm is None and state != 'absent': module.fail_json(msg='There is no VM with id=' + str(vm_id)) vms.append(vm) return vms def get_vm_info(client, vm): vm = client.vm.info(vm.ID) networks_info = [] disk_size = [] if 'DISK' in vm.TEMPLATE: if isinstance(vm.TEMPLATE['DISK'], list): for disk in vm.TEMPLATE['DISK']: disk_size.append(disk['SIZE'] + ' MB') else: disk_size.append(vm.TEMPLATE['DISK']['SIZE'] + ' MB') if 'NIC' in vm.TEMPLATE: if isinstance(vm.TEMPLATE['NIC'], list): for nic in vm.TEMPLATE['NIC']: networks_info.append({ 'ip': nic.get('IP', ''), 'mac': nic.get('MAC', ''), 'name': nic.get('NETWORK', ''), 'security_groups': nic.get('SECURITY_GROUPS', '') }) else: networks_info.append({ 'ip': vm.TEMPLATE['NIC'].get('IP', ''), 'mac': vm.TEMPLATE['NIC'].get('MAC', ''), 'name': vm.TEMPLATE['NIC'].get('NETWORK', ''), 'security_groups': vm.TEMPLATE['NIC'].get('SECURITY_GROUPS', '') }) import time current_time = time.localtime() vm_start_time = time.localtime(vm.STIME) vm_uptime = time.mktime(current_time) - time.mktime(vm_start_time) vm_uptime /= (60 * 60) permissions_str = parse_vm_permissions(client, vm) # LCM_STATE is VM's sub-state that is relevant only when STATE is ACTIVE vm_lcm_state = None if vm.STATE == VM_STATES.index('ACTIVE'): vm_lcm_state = LCM_STATES[vm.LCM_STATE] vm_labels, vm_attributes = get_vm_labels_and_attributes_dict(client, vm.ID) updateconf = parse_updateconf(vm.TEMPLATE) info = { 'template_id': int(vm.TEMPLATE['TEMPLATE_ID']), 'vm_id': vm.ID, 'vm_name': vm.NAME, 'state': VM_STATES[vm.STATE], 'lcm_state': vm_lcm_state, 'owner_name': vm.UNAME, 'owner_id': vm.UID, 'networks': networks_info, 'disk_size': disk_size, 'memory': vm.TEMPLATE['MEMORY'] + ' MB', 'vcpu': vm.TEMPLATE['VCPU'], 'cpu': vm.TEMPLATE['CPU'], 'group_name': vm.GNAME, 'group_id': vm.GID, 'uptime_h': int(vm_uptime), 'attributes': vm_attributes, 'mode': permissions_str, 'labels': vm_labels, 'updateconf': updateconf, } return info def parse_vm_permissions(client, vm): vm_PERMISSIONS = client.vm.info(vm.ID).PERMISSIONS owner_octal = int(vm_PERMISSIONS.OWNER_U) * 4 + int(vm_PERMISSIONS.OWNER_M) * 2 + int(vm_PERMISSIONS.OWNER_A) group_octal = int(vm_PERMISSIONS.GROUP_U) * 4 + int(vm_PERMISSIONS.GROUP_M) * 2 + int(vm_PERMISSIONS.GROUP_A) other_octal = int(vm_PERMISSIONS.OTHER_U) * 4 + int(vm_PERMISSIONS.OTHER_M) * 2 + int(vm_PERMISSIONS.OTHER_A) permissions = str(owner_octal) + str(group_octal) + str(other_octal) return permissions def set_vm_permissions(module, client, vms, permissions): changed = False for vm in vms: vm = client.vm.info(vm.ID) old_permissions = parse_vm_permissions(client, vm) changed = changed or old_permissions != permissions if not module.check_mode and old_permissions != permissions: permissions_str = bin(int(permissions, base=8))[2:] # 600 -> 110000000 mode_bits = [int(d) for d in permissions_str] try: client.vm.chmod( vm.ID, mode_bits[0], mode_bits[1], mode_bits[2], mode_bits[3], mode_bits[4], mode_bits[5], mode_bits[6], mode_bits[7], mode_bits[8]) except pyone.OneAuthorizationException: module.fail_json(msg="Permissions changing is unsuccessful, but instances are present if you deployed them.") return changed def set_vm_ownership(module, client, vms, owner_id, group_id): changed = False for vm in vms: vm = client.vm.info(vm.ID) if owner_id is None: owner_id = vm.UID if group_id is None: group_id = vm.GID changed = changed or owner_id != vm.UID or group_id != vm.GID if not module.check_mode and (owner_id != vm.UID or group_id != vm.GID): try: client.vm.chown(vm.ID, owner_id, group_id) except pyone.OneAuthorizationException: module.fail_json(msg="Ownership changing is unsuccessful, but instances are present if you deployed them.") return changed def update_vm(module, client, vm, updateconf_dict): changed = False if not updateconf_dict: return changed before = client.vm.info(vm.ID).TEMPLATE client.vm.updateconf(vm.ID, render(updateconf_dict), 1) # 1: Merge new template with the existing one. after = client.vm.info(vm.ID).TEMPLATE changed = before != after return changed def update_vms(module, client, vms, *args): changed = False for vm in vms: changed = update_vm(module, client, vm, *args) or changed return changed def get_size_in_MB(module, size_str): SYMBOLS = ['B', 'KB', 'MB', 'GB', 'TB'] s = size_str init = size_str num = "" while s and s[0:1].isdigit() or s[0:1] == '.': num += s[0] s = s[1:] num = float(num) symbol = s.strip() if symbol not in SYMBOLS: module.fail_json(msg="Cannot interpret %r %r %d" % (init, symbol, num)) prefix = {'B': 1} for i, s in enumerate(SYMBOLS[1:]): prefix[s] = 1 << (i + 1) * 10 size_in_bytes = int(num * prefix[symbol]) size_in_MB = size_in_bytes / (1024 * 1024) return size_in_MB def create_vm(module, client, template_id, attributes_dict, labels_list, disk_size, network_attrs_list, vm_start_on_hold, vm_persistent, updateconf_dict): if attributes_dict: vm_name = attributes_dict.get('NAME', '') template = client.template.info(template_id).TEMPLATE disk_count = len(flatten(template.get('DISK', []))) if disk_size: size_count = len(flatten(disk_size)) # check if the number of disks is correct if disk_count != size_count: module.fail_json(msg='This template has ' + str(disk_count) + ' disks but you defined ' + str(size_count)) vm_extra_template = dict_merge(template or {}, attributes_dict or {}) vm_extra_template = dict_merge(vm_extra_template, { 'LABELS': ','.join(labels_list), 'NIC': flatten(network_attrs_list, extract=True), 'DISK': flatten([ disk if not size else dict_merge(disk, { 'SIZE': str(int(get_size_in_MB(module, size))), }) for disk, size in zip( flatten(template.get('DISK', [])), flatten(disk_size or [None] * disk_count), ) if disk is not None ], extract=True) }) vm_extra_template = dict_merge(vm_extra_template, updateconf_dict or {}) try: vm_id = client.template.instantiate(template_id, vm_name, vm_start_on_hold, render(vm_extra_template), vm_persistent) except pyone.OneException as e: module.fail_json(msg=str(e)) vm = get_vm_by_id(client, vm_id) return get_vm_info(client, vm) def generate_next_index(vm_filled_indexes_list, num_sign_cnt): counter = 0 cnt_str = str(counter).zfill(num_sign_cnt) while cnt_str in vm_filled_indexes_list: counter = counter + 1 cnt_str = str(counter).zfill(num_sign_cnt) return cnt_str def get_vm_labels_and_attributes_dict(client, vm_id): vm_USER_TEMPLATE = client.vm.info(vm_id).USER_TEMPLATE attrs_dict = {} labels_list = [] for key, value in vm_USER_TEMPLATE.items(): if key != 'LABELS': attrs_dict[key] = value else: if key is not None and value is not None: labels_list = value.split(',') return labels_list, attrs_dict def get_all_vms_by_attributes(client, attributes_dict, labels_list): pool = client.vmpool.info(-2, -1, -1, -1).VM vm_list = [] name = '' if attributes_dict: name = attributes_dict.pop('NAME', '') if name != '': base_name = name[:len(name) - name.count('#')] # Check does the name have indexed format with_hash = name.endswith('#') for vm in pool: if vm.NAME.startswith(base_name): if with_hash and vm.NAME[len(base_name):].isdigit(): # If the name has indexed format and after base_name it has only digits it'll be matched vm_list.append(vm) elif not with_hash and vm.NAME == name: # If the name is not indexed it has to be same vm_list.append(vm) pool = vm_list import copy vm_list = copy.copy(pool) for vm in pool: remove_list = [] vm_labels_list, vm_attributes_dict = get_vm_labels_and_attributes_dict(client, vm.ID) if attributes_dict and len(attributes_dict) > 0: for key, val in attributes_dict.items(): if key in vm_attributes_dict: if val and vm_attributes_dict[key] != val: remove_list.append(vm) break else: remove_list.append(vm) break vm_list = list(set(vm_list).difference(set(remove_list))) remove_list = [] if labels_list and len(labels_list) > 0: for label in labels_list: if label not in vm_labels_list: remove_list.append(vm) break vm_list = list(set(vm_list).difference(set(remove_list))) return vm_list def create_count_of_vms(module, client, template_id, count, attributes_dict, labels_list, disk_size, network_attrs_list, wait, wait_timeout, vm_start_on_hold, vm_persistent, updateconf_dict): new_vms_list = [] vm_name = '' if attributes_dict: vm_name = attributes_dict.get('NAME', '') if module.check_mode: return True, [], [] # Create list of used indexes vm_filled_indexes_list = None num_sign_cnt = vm_name.count('#') if vm_name != '' and num_sign_cnt > 0: vm_list = get_all_vms_by_attributes(client, {'NAME': vm_name}, None) base_name = vm_name[:len(vm_name) - num_sign_cnt] vm_name = base_name # Make list which contains used indexes in format ['000', '001',...] vm_filled_indexes_list = list((vm.NAME[len(base_name):].zfill(num_sign_cnt)) for vm in vm_list) while count > 0: new_vm_name = vm_name # Create indexed name if vm_filled_indexes_list is not None: next_index = generate_next_index(vm_filled_indexes_list, num_sign_cnt) vm_filled_indexes_list.append(next_index) new_vm_name += next_index # Update NAME value in the attributes in case there is index attributes_dict['NAME'] = new_vm_name new_vm_dict = create_vm(module, client, template_id, attributes_dict, labels_list, disk_size, network_attrs_list, vm_start_on_hold, vm_persistent, updateconf_dict) new_vm_id = new_vm_dict.get('vm_id') new_vm = get_vm_by_id(client, new_vm_id) new_vms_list.append(new_vm) count -= 1 if vm_start_on_hold: if wait: for vm in new_vms_list: wait_for_hold(module, client, vm, wait_timeout) else: if wait: for vm in new_vms_list: wait_for_running(module, client, vm, wait_timeout) return True, new_vms_list, [] def create_exact_count_of_vms(module, client, template_id, exact_count, attributes_dict, count_attributes_dict, labels_list, count_labels_list, disk_size, network_attrs_list, hard, wait, wait_timeout, vm_start_on_hold, vm_persistent, updateconf_dict): vm_list = get_all_vms_by_attributes(client, count_attributes_dict, count_labels_list) vm_count_diff = exact_count - len(vm_list) changed = vm_count_diff != 0 new_vms_list = [] instances_list = [] tagged_instances_list = vm_list if module.check_mode: return changed, instances_list, tagged_instances_list if vm_count_diff > 0: # Add more VMs changed, instances_list, tagged_instances = create_count_of_vms(module, client, template_id, vm_count_diff, attributes_dict, labels_list, disk_size, network_attrs_list, wait, wait_timeout, vm_start_on_hold, vm_persistent, updateconf_dict) tagged_instances_list += instances_list elif vm_count_diff < 0: # Delete surplus VMs old_vms_list = [] while vm_count_diff < 0: old_vm = vm_list.pop(0) old_vms_list.append(old_vm) terminate_vm(module, client, old_vm, hard) vm_count_diff += 1 if wait: for vm in old_vms_list: wait_for_done(module, client, vm, wait_timeout) instances_list = old_vms_list # store only the remaining instances old_vms_set = set(old_vms_list) tagged_instances_list = [vm for vm in vm_list if vm not in old_vms_set] return changed, instances_list, tagged_instances_list VM_STATES = ['INIT', 'PENDING', 'HOLD', 'ACTIVE', 'STOPPED', 'SUSPENDED', 'DONE', '', 'POWEROFF', 'UNDEPLOYED', 'CLONING', 'CLONING_FAILURE'] LCM_STATES = ['LCM_INIT', 'PROLOG', 'BOOT', 'RUNNING', 'MIGRATE', 'SAVE_STOP', 'SAVE_SUSPEND', 'SAVE_MIGRATE', 'PROLOG_MIGRATE', 'PROLOG_RESUME', 'EPILOG_STOP', 'EPILOG', 'SHUTDOWN', 'STATE13', 'STATE14', 'CLEANUP_RESUBMIT', 'UNKNOWN', 'HOTPLUG', 'SHUTDOWN_POWEROFF', 'BOOT_UNKNOWN', 'BOOT_POWEROFF', 'BOOT_SUSPENDED', 'BOOT_STOPPED', 'CLEANUP_DELETE', 'HOTPLUG_SNAPSHOT', 'HOTPLUG_NIC', 'HOTPLUG_SAVEAS', 'HOTPLUG_SAVEAS_POWEROFF', 'HOTPULG_SAVEAS_SUSPENDED', 'SHUTDOWN_UNDEPLOY'] def wait_for_state(module, client, vm, wait_timeout, state_predicate): import time start_time = time.time() while (time.time() - start_time) < wait_timeout: vm = client.vm.info(vm.ID) state = vm.STATE lcm_state = vm.LCM_STATE if state_predicate(state, lcm_state): return vm elif state not in [VM_STATES.index('INIT'), VM_STATES.index('PENDING'), VM_STATES.index('HOLD'), VM_STATES.index('ACTIVE'), VM_STATES.index('CLONING'), VM_STATES.index('POWEROFF')]: module.fail_json(msg='Action is unsuccessful. VM state: ' + VM_STATES[state]) time.sleep(1) module.fail_json(msg="Wait timeout has expired!") def wait_for_running(module, client, vm, wait_timeout): return wait_for_state(module, client, vm, wait_timeout, lambda state, lcm_state: (state in [VM_STATES.index('ACTIVE')] and lcm_state in [LCM_STATES.index('RUNNING')])) def wait_for_done(module, client, vm, wait_timeout): return wait_for_state(module, client, vm, wait_timeout, lambda state, lcm_state: (state in [VM_STATES.index('DONE')])) def wait_for_hold(module, client, vm, wait_timeout): return wait_for_state(module, client, vm, wait_timeout, lambda state, lcm_state: (state in [VM_STATES.index('HOLD')])) def wait_for_poweroff(module, client, vm, wait_timeout): return wait_for_state(module, client, vm, wait_timeout, lambda state, lcm_state: (state in [VM_STATES.index('POWEROFF')])) def terminate_vm(module, client, vm, hard=False): changed = False if not vm: return changed changed = True if not module.check_mode: if hard: client.vm.action('terminate-hard', vm.ID) else: client.vm.action('terminate', vm.ID) return changed def terminate_vms(module, client, vms, hard): changed = False for vm in vms: changed = terminate_vm(module, client, vm, hard) or changed return changed def poweroff_vm(module, client, vm, hard): vm = client.vm.info(vm.ID) changed = False lcm_state = vm.LCM_STATE state = vm.STATE if lcm_state not in [LCM_STATES.index('SHUTDOWN'), LCM_STATES.index('SHUTDOWN_POWEROFF')] and state not in [VM_STATES.index('POWEROFF')]: changed = True if changed and not module.check_mode: if not hard: client.vm.action('poweroff', vm.ID) else: client.vm.action('poweroff-hard', vm.ID) return changed def poweroff_vms(module, client, vms, hard): changed = False for vm in vms: changed = poweroff_vm(module, client, vm, hard) or changed return changed def reboot_vms(module, client, vms, wait_timeout, hard): if not module.check_mode: # Firstly, power-off all instances for vm in vms: vm = client.vm.info(vm.ID) lcm_state = vm.LCM_STATE state = vm.STATE if lcm_state not in [LCM_STATES.index('SHUTDOWN_POWEROFF')] and state not in [VM_STATES.index('POWEROFF')]: poweroff_vm(module, client, vm, hard) # Wait for all to be power-off for vm in vms: wait_for_poweroff(module, client, vm, wait_timeout) for vm in vms: resume_vm(module, client, vm) return True def resume_vm(module, client, vm): vm = client.vm.info(vm.ID) changed = False state = vm.STATE if state in [VM_STATES.index('HOLD')]: changed = release_vm(module, client, vm) return changed lcm_state = vm.LCM_STATE if lcm_state == LCM_STATES.index('SHUTDOWN_POWEROFF'): module.fail_json(msg="Cannot perform action 'resume' because this action is not available " + "for LCM_STATE: 'SHUTDOWN_POWEROFF'. Wait for the VM to shutdown properly") if lcm_state not in [LCM_STATES.index('RUNNING')]: changed = True if changed and not module.check_mode: client.vm.action('resume', vm.ID) return changed def resume_vms(module, client, vms): changed = False for vm in vms: changed = resume_vm(module, client, vm) or changed return changed def release_vm(module, client, vm): vm = client.vm.info(vm.ID) changed = False state = vm.STATE if state != VM_STATES.index('HOLD'): module.fail_json(msg="Cannot perform action 'release' because this action is not available " + "because VM is not in state 'HOLD'.") else: changed = True if changed and not module.check_mode: client.vm.action('release', vm.ID) return changed def check_name_attribute(module, attributes): if attributes.get("NAME"): import re if re.match(r'^[^#]+#*$', attributes.get("NAME")) is None: module.fail_json(msg="Illegal 'NAME' attribute: '" + attributes.get("NAME") + "' .Signs '#' are allowed only at the end of the name and the name cannot contain only '#'.") TEMPLATE_RESTRICTED_ATTRIBUTES = ["CPU", "VCPU", "OS", "FEATURES", "MEMORY", "DISK", "NIC", "INPUT", "GRAPHICS", "CONTEXT", "CREATED_BY", "CPU_COST", "DISK_COST", "MEMORY_COST", "TEMPLATE_ID", "VMID", "AUTOMATIC_DS_REQUIREMENTS", "DEPLOY_FOLDER", "LABELS"] def check_attributes(module, attributes): for key in attributes.keys(): if key in TEMPLATE_RESTRICTED_ATTRIBUTES: module.fail_json(msg='Restricted attribute `' + key + '` cannot be used when filtering VMs.') # Check the format of the name attribute check_name_attribute(module, attributes) def disk_save_as(module, client, vm, disk_saveas, wait_timeout): if not disk_saveas.get('name'): module.fail_json(msg="Key 'name' is required for 'disk_saveas' option") image_name = disk_saveas.get('name') disk_id = disk_saveas.get('disk_id', 0) if not module.check_mode: if vm.STATE != VM_STATES.index('POWEROFF'): module.fail_json(msg="'disksaveas' option can be used only when the VM is in 'POWEROFF' state") try: client.vm.disksaveas(vm.ID, disk_id, image_name, 'OS', -1) except pyone.OneException as e: module.fail_json(msg=str(e)) wait_for_poweroff(module, client, vm, wait_timeout) # wait for VM to leave the hotplug_saveas_poweroff state def get_connection_info(module): url = module.params.get('api_url') username = module.params.get('api_username') password = module.params.get('api_password') if not url: url = os.environ.get('ONE_URL') if not username: username = os.environ.get('ONE_USERNAME') if not password: password = os.environ.get('ONE_PASSWORD') if not username: if not password: authfile = os.environ.get('ONE_AUTH') if authfile is None: authfile = os.path.join(os.environ.get("HOME"), ".one", "one_auth") try: with open(authfile, "r") as fp: authstring = fp.read().rstrip() username = authstring.split(":")[0] password = authstring.split(":")[1] except (OSError, IOError): module.fail_json(msg=("Could not find or read ONE_AUTH file at '%s'" % authfile)) except Exception: module.fail_json(msg=("Error occurs when read ONE_AUTH file at '%s'" % authfile)) if not url: module.fail_json(msg="Opennebula API url (api_url) is not specified") from collections import namedtuple auth_params = namedtuple('auth', ('url', 'username', 'password')) return auth_params(url=url, username=username, password=password) def main(): fields = { "api_url": {"required": False, "type": "str"}, "api_username": {"required": False, "type": "str"}, "api_password": {"required": False, "type": "str", "no_log": True}, "instance_ids": {"required": False, "aliases": ['ids'], "type": "list", "elements": "int"}, "template_name": {"required": False, "type": "str"}, "template_id": {"required": False, "type": "int"}, "vm_start_on_hold": {"default": False, "type": "bool"}, "state": { "default": "present", "choices": ['present', 'absent', 'rebooted', 'poweredoff', 'running'], "type": "str" }, "mode": {"required": False, "type": "str"}, "owner_id": {"required": False, "type": "int"}, "group_id": {"required": False, "type": "int"}, "wait": {"default": True, "type": "bool"}, "wait_timeout": {"default": 300, "type": "int"}, "hard": {"default": False, "type": "bool"}, "memory": {"required": False, "type": "str"}, "cpu": {"required": False, "type": "float"}, "vcpu": {"required": False, "type": "int"}, "disk_size": {"required": False, "type": "list", "elements": "str"}, "datastore_name": {"required": False, "type": "str"}, "datastore_id": {"required": False, "type": "int"}, "networks": {"default": [], "type": "list", "elements": "dict"}, "count": {"default": 1, "type": "int"}, "exact_count": {"required": False, "type": "int"}, "attributes": {"default": {}, "type": "dict"}, "count_attributes": {"required": False, "type": "dict"}, "labels": {"default": [], "type": "list", "elements": "str"}, "count_labels": {"required": False, "type": "list", "elements": "str"}, "disk_saveas": {"type": "dict"}, "persistent": {"default": False, "type": "bool"}, "updateconf": {"type": "dict"}, } module = AnsibleModule(argument_spec=fields, mutually_exclusive=[ ['template_id', 'template_name', 'instance_ids'], ['template_id', 'template_name', 'disk_saveas'], ['instance_ids', 'count_attributes', 'count'], ['instance_ids', 'count_labels', 'count'], ['instance_ids', 'exact_count'], ['instance_ids', 'attributes'], ['instance_ids', 'labels'], ['disk_saveas', 'attributes'], ['disk_saveas', 'labels'], ['exact_count', 'count'], ['count', 'hard'], ['instance_ids', 'cpu'], ['instance_ids', 'vcpu'], ['instance_ids', 'memory'], ['instance_ids', 'disk_size'], ['instance_ids', 'networks'], ['persistent', 'disk_size'] ], supports_check_mode=True) if not HAS_PYONE: module.fail_json(msg='This module requires pyone to work!') auth = get_connection_info(module) params = module.params instance_ids = params.get('instance_ids') requested_template_name = params.get('template_name') requested_template_id = params.get('template_id') put_vm_on_hold = params.get('vm_start_on_hold') state = params.get('state') permissions = params.get('mode') owner_id = params.get('owner_id') group_id = params.get('group_id') wait = params.get('wait') wait_timeout = params.get('wait_timeout') hard = params.get('hard') memory = params.get('memory') cpu = params.get('cpu') vcpu = params.get('vcpu') disk_size = params.get('disk_size') requested_datastore_id = params.get('datastore_id') requested_datastore_name = params.get('datastore_name') networks = params.get('networks') count = params.get('count') exact_count = params.get('exact_count') attributes = params.get('attributes') count_attributes = params.get('count_attributes') labels = params.get('labels') count_labels = params.get('count_labels') disk_saveas = params.get('disk_saveas') persistent = params.get('persistent') updateconf = params.get('updateconf') if not (auth.username and auth.password): module.warn("Credentials missing") else: one_client = pyone.OneServer(auth.url, session=auth.username + ':' + auth.password) if attributes: attributes = dict((key.upper(), value) for key, value in attributes.items()) check_attributes(module, attributes) if count_attributes: count_attributes = dict((key.upper(), value) for key, value in count_attributes.items()) if not attributes: import copy module.warn('When you pass `count_attributes` without `attributes` option when deploying, `attributes` option will have same values implicitly.') attributes = copy.copy(count_attributes) check_attributes(module, count_attributes) if updateconf: check_updateconf(module, updateconf) if count_labels and not labels: module.warn('When you pass `count_labels` without `labels` option when deploying, `labels` option will have same values implicitly.') labels = count_labels # Fetch template template_id = None if requested_template_id is not None or requested_template_name: template_id = get_template_id(module, one_client, requested_template_id, requested_template_name) if template_id is None: if requested_template_id is not None: module.fail_json(msg='There is no template with template_id: ' + str(requested_template_id)) elif requested_template_name: module.fail_json(msg="There is no template with name: " + requested_template_name) # Fetch datastore datastore_id = None if requested_datastore_id or requested_datastore_name: datastore_id = get_datastore_id(module, one_client, requested_datastore_id, requested_datastore_name) if datastore_id is None: if requested_datastore_id: module.fail_json(msg='There is no datastore with datastore_id: ' + str(requested_datastore_id)) elif requested_datastore_name: module.fail_json(msg="There is no datastore with name: " + requested_datastore_name) else: attributes['SCHED_DS_REQUIREMENTS'] = 'ID=' + str(datastore_id) if exact_count and template_id is None: module.fail_json(msg='Option `exact_count` needs template_id or template_name') if exact_count is not None and not (count_attributes or count_labels): module.fail_json(msg='Either `count_attributes` or `count_labels` has to be specified with option `exact_count`.') if (count_attributes or count_labels) and exact_count is None: module.fail_json(msg='Option `exact_count` has to be specified when either `count_attributes` or `count_labels` is used.') if template_id is not None and state != 'present': module.fail_json(msg="Only state 'present' is valid for the template") if memory: attributes['MEMORY'] = str(int(get_size_in_MB(module, memory))) if cpu: attributes['CPU'] = str(cpu) if vcpu: attributes['VCPU'] = str(vcpu) if exact_count is not None and state != 'present': module.fail_json(msg='The `exact_count` option is valid only for the `present` state') if exact_count is not None and exact_count < 0: module.fail_json(msg='`exact_count` cannot be less than 0') if count <= 0: module.fail_json(msg='`count` has to be greater than 0') if permissions is not None: import re if re.match("^[0-7]{3}$", permissions) is None: module.fail_json(msg="Option `mode` has to have exactly 3 digits and be in the octet format e.g. 600") if exact_count is not None: # Deploy an exact count of VMs changed, instances_list, tagged_instances_list = create_exact_count_of_vms(module, one_client, template_id, exact_count, attributes, count_attributes, labels, count_labels, disk_size, networks, hard, wait, wait_timeout, put_vm_on_hold, persistent, updateconf) vms = tagged_instances_list elif template_id is not None and state == 'present': # Deploy count VMs changed, instances_list, tagged_instances_list = create_count_of_vms(module, one_client, template_id, count, attributes, labels, disk_size, networks, wait, wait_timeout, put_vm_on_hold, persistent, updateconf) # instances_list - new instances # tagged_instances_list - all instances with specified `count_attributes` and `count_labels` vms = instances_list else: # Fetch data of instances, or change their state if not (instance_ids or attributes or labels): module.fail_json(msg="At least one of `instance_ids`,`attributes`,`labels` must be passed!") if memory or cpu or vcpu or disk_size or networks: module.fail_json(msg="Parameters as `memory`, `cpu`, `vcpu`, `disk_size` and `networks` you can only set when deploying a VM!") if hard and state not in ['rebooted', 'poweredoff', 'absent', 'present']: module.fail_json(msg="The 'hard' option can be used only for one of these states: 'rebooted', 'poweredoff', 'absent' and 'present'") vms = [] tagged = False changed = False if instance_ids: vms = get_vms_by_ids(module, one_client, state, instance_ids) else: tagged = True vms = get_all_vms_by_attributes(one_client, attributes, labels) if len(vms) == 0 and state != 'absent' and state != 'present': module.fail_json(msg='There are no instances with specified `instance_ids`, `attributes` and/or `labels`') if len(vms) == 0 and state == 'present' and not tagged: module.fail_json(msg='There are no instances with specified `instance_ids`.') if tagged and state == 'absent': module.fail_json(msg='Option `instance_ids` is required when state is `absent`.') if state == 'absent': changed = terminate_vms(module, one_client, vms, hard) elif state == 'rebooted': changed = reboot_vms(module, one_client, vms, wait_timeout, hard) elif state == 'poweredoff': changed = poweroff_vms(module, one_client, vms, hard) elif state == 'running': changed = resume_vms(module, one_client, vms) instances_list = vms tagged_instances_list = [] if permissions is not None: changed = set_vm_permissions(module, one_client, vms, permissions) or changed if owner_id is not None or group_id is not None: changed = set_vm_ownership(module, one_client, vms, owner_id, group_id) or changed if template_id is None and updateconf is not None: changed = update_vms(module, one_client, vms, updateconf) or changed if wait and not module.check_mode and state != 'present': wait_for = { 'absent': wait_for_done, 'rebooted': wait_for_running, 'poweredoff': wait_for_poweroff, 'running': wait_for_running } for vm in vms: if vm is not None: wait_for[state](module, one_client, vm, wait_timeout) if disk_saveas is not None: if len(vms) == 0: module.fail_json(msg="There is no VM whose disk will be saved.") disk_save_as(module, one_client, vms[0], disk_saveas, wait_timeout) changed = True # instances - a list of instances info whose state is changed or which are fetched with C(instance_ids) option instances = list(get_vm_info(one_client, vm) for vm in instances_list if vm is not None) instances_ids = list(vm.ID for vm in instances_list if vm is not None) # tagged_instances - A list of instances info based on a specific attributes and/or labels that are specified with C(count_attributes) and C(count_labels) tagged_instances = list(get_vm_info(one_client, vm) for vm in tagged_instances_list if vm is not None) result = {'changed': changed, 'instances': instances, 'instances_ids': instances_ids, 'tagged_instances': tagged_instances} module.exit_json(**result) if __name__ == '__main__': main()