#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 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 # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type """ (c) 2017, Milan Ilic (c) 2019, Jan Meerkamp This file is part of Ansible Ansible is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Ansible is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Ansible. If not, see . """ DOCUMENTATION = ''' --- module: one_vm short_description: Creates or terminates OpenNebula instances description: - Manages OpenNebula instances requirements: - pyone 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 C(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 C(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 C(ONE_PASSWORD) environment variable is used. - if both I(api_username) or I(api_password) are not set, then it will try - authenticate with ONE auth file. Default path is "~/.one/one_auth". - Set environment variable C(ONE_AUTH) to override this path. type: str template_name: description: - Name of VM template to use to create a new instace 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':' C(absent), C(running), C(rebooted), C(poweredoff) aliases: ['ids'] type: list elements: int state: description: - C(present) - create instances from a template specified with C(template_id)/C(template_name). - C(running) - run instances - C(poweredoff) - power-off instances - C(rebooted) - reboot instances - C(absent) - terminate instances choices: ["present", "absent", "running", "rebooted", "poweredoff"] default: present type: str hard: description: - Reboot, power-off or terminate instances C(hard) default: no 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: yes 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 C(count_attributes) and C(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 - C(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 C(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 C(count_attributes) and - C(count_labels) parameters should be deployed. Instances are either - created or terminated based on this value. - NOTE':' Instances with the least IDs will be terminated first. type: int mode: description: - Set permission mode of the instance in octet format, e.g. C(600) 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,...). - NOTE':' If The Template hats Multiple Disks the Order of the Sizes is - matched against the order specified in C(template_id)/C(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. - I(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: NO type: bool version_added: '0.2.0' datastore_id: description: - Name of Datastore to use to create a new instace version_added: '0.2.0' type: int datastore_name: description: - Name of Datastore to use to create a new instace version_added: '0.2.0' type: str 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: yes - 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 ''' RETURN = ''' instances_ids: description: a list of instances ids whose state is changed or which are fetched with C(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 C(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 } tagged_instances: description: - A list of instances info based on a specific attributes and/or - labels that are specified with C(count_attributes) and C(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 } ''' try: import pyone HAS_PYONE = True except ImportError: HAS_PYONE = False from ansible.module_utils.basic import AnsibleModule import os 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) 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 } 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 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_disk_str(module, client, template_id, disk_size_list): if not disk_size_list: return '' template = client.template.info(template_id) if isinstance(template.TEMPLATE['DISK'], list): # check if the number of disks is correct if len(template.TEMPLATE['DISK']) != len(disk_size_list): module.fail_json(msg='This template has ' + str(len(template.TEMPLATE['DISK'])) + ' disks but you defined ' + str(len(disk_size_list))) result = '' index = 0 for DISKS in template.TEMPLATE['DISK']: disk = {} diskresult = '' # Get all info about existed disk e.g. IMAGE_ID,... for key, value in DISKS.items(): disk[key] = value # copy disk attributes if it is not the size attribute diskresult += 'DISK = [' + ','.join('{key}="{val}"'.format(key=key, val=val) for key, val in disk.items() if key != 'SIZE') # Set the Disk Size diskresult += ', SIZE=' + str(int(get_size_in_MB(module, disk_size_list[index]))) + ']\n' result += diskresult index += 1 else: if len(disk_size_list) > 1: module.fail_json(msg='This template has one disk but you defined ' + str(len(disk_size_list))) disk = {} # Get all info about existed disk e.g. IMAGE_ID,... for key, value in template.TEMPLATE['DISK'].items(): disk[key] = value # copy disk attributes if it is not the size attribute result = 'DISK = [' + ','.join('{key}="{val}"'.format(key=key, val=val) for key, val in disk.items() if key != 'SIZE') # Set the Disk Size result += ', SIZE=' + str(int(get_size_in_MB(module, disk_size_list[0]))) + ']\n' return result def create_attributes_str(attributes_dict, labels_list): attributes_str = '' if labels_list: attributes_str += 'LABELS="' + ','.join('{label}'.format(label=label) for label in labels_list) + '"\n' if attributes_dict: attributes_str += '\n'.join('{key}="{val}"'.format(key=key.upper(), val=val) for key, val in attributes_dict.items()) + '\n' return attributes_str def create_nics_str(network_attrs_list): nics_str = '' for network in network_attrs_list: # Packing key-value dict in string with format key="value", key="value" network_str = ','.join('{key}="{val}"'.format(key=key, val=val) for key, val in network.items()) nics_str = nics_str + 'NIC = [' + network_str + ']\n' return nics_str def create_vm(module, client, template_id, attributes_dict, labels_list, disk_size, network_attrs_list, vm_start_on_hold, vm_persistent): if attributes_dict: vm_name = attributes_dict.get('NAME', '') disk_str = create_disk_str(module, client, template_id, disk_size) vm_extra_template_str = create_attributes_str(attributes_dict, labels_list) + create_nics_str(network_attrs_list) + disk_str try: vm_id = client.template.instantiate(template_id, vm_name, vm_start_on_hold, vm_extra_template_str, 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: 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): 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) 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): 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) 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="Ilegal '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"} } 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') 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 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) 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) # 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 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()