#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2017, Milan Ilic # 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_service short_description: Deploy and manage OpenNebula services description: - Manage OpenNebula services extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: full diff_mode: support: none options: api_url: description: - URL of the OpenNebula OneFlow API 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(ONEFLOW_URL) environment variable is used. type: str api_username: description: - Name of the user to login into the OpenNebula OneFlow API server. If not set then the value of the E(ONEFLOW_USERNAME) environment variable is used. type: str api_password: description: - Password of the user to login into OpenNebula OneFlow API server. If not set then the value of the E(ONEFLOW_PASSWORD) environment variable is used. type: str template_name: description: - Name of service template to use to create a new instance of a service. type: str template_id: description: - ID of a service template to use to create a new instance of a service. type: int service_id: description: - ID of a service instance that you would like to manage. type: int service_name: description: - Name of a service instance that you would like to manage. type: str unique: description: - Setting O(unique=true) will make sure that there is only one service instance running with a name set with O(service_name) when instantiating a service from a template specified with O(template_id) or O(template_name). Check examples below. type: bool default: false state: description: - V(present) - instantiate a service from a template specified with O(template_id) or O(template_name). - V(absent) - terminate an instance of a service specified with O(template_id) or O(template_name). choices: ["present", "absent"] default: present type: str mode: description: - Set permission mode of a service 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 service. type: int group_id: description: - ID of the group which will be set as the group of the service. type: int wait: description: - Wait for the instance to reach RUNNING state after DEPLOYING or COOLDOWN state after SCALING. type: bool default: false wait_timeout: description: - How long before wait gives up, in seconds. default: 300 type: int custom_attrs: description: - Dictionary of key/value custom attributes which will be used when instantiating a new service. default: {} type: dict role: description: - Name of the role whose cardinality should be changed. type: str cardinality: description: - Number of VMs for the specified role. type: int force: description: - Force the new cardinality even if it is outside the limits. type: bool default: false author: - "Milan Ilic (@ilicmilan)" ''' EXAMPLES = ''' - name: Instantiate a new service community.general.one_service: template_id: 90 register: result - name: Print service properties ansible.builtin.debug: msg: result - name: Instantiate a new service with specified service_name, service group and mode community.general.one_service: template_name: 'app1_template' service_name: 'app1' group_id: 1 mode: '660' - name: Instantiate a new service with template_id and pass custom_attrs dict community.general.one_service: template_id: 90 custom_attrs: public_network_id: 21 private_network_id: 26 - name: Instantiate a new service 'foo' if the service doesn't already exist, otherwise do nothing community.general.one_service: template_id: 53 service_name: 'foo' unique: true - name: Delete a service by ID community.general.one_service: service_id: 153 state: absent - name: Get service info community.general.one_service: service_id: 153 register: service_info - name: Change service owner, group and mode community.general.one_service: service_name: 'app2' owner_id: 34 group_id: 113 mode: '600' - name: Instantiate service and wait for it to become RUNNING community.general.one_service: template_id: 43 service_name: 'foo1' - name: Wait service to become RUNNING community.general.one_service: service_id: 112 wait: true - name: Change role cardinality community.general.one_service: service_id: 153 role: bar cardinality: 5 - name: Change role cardinality and wait for it to be applied community.general.one_service: service_id: 112 role: foo cardinality: 7 wait: true ''' RETURN = ''' service_id: description: service id type: int returned: success sample: 153 service_name: description: service name type: str returned: success sample: app1 group_id: description: service's group id type: int returned: success sample: 1 group_name: description: service's group name type: str returned: success sample: one-users owner_id: description: service's owner id type: int returned: success sample: 143 owner_name: description: service's owner name type: str returned: success sample: ansible-test state: description: state of service instance type: str returned: success sample: RUNNING mode: description: service's mode type: int returned: success sample: 660 roles: description: list of dictionaries of roles, each role is described by name, cardinality, state and nodes ids type: list returned: success sample: - {"cardinality": 1,"name": "foo","state": "RUNNING", "ids": [ 123, 456 ]} - {"cardinality": 2,"name": "bar","state": "RUNNING", "ids": [ 452, 567, 746 ]} ''' import os from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import open_url STATES = ("PENDING", "DEPLOYING", "RUNNING", "UNDEPLOYING", "WARNING", "DONE", "FAILED_UNDEPLOYING", "FAILED_DEPLOYING", "SCALING", "FAILED_SCALING", "COOLDOWN") def get_all_templates(module, auth): try: all_templates = open_url(url=(auth.url + "/service_template"), method="GET", force_basic_auth=True, url_username=auth.user, url_password=auth.password) except Exception as e: module.fail_json(msg=str(e)) return module.from_json(all_templates.read()) def get_template(module, auth, pred): all_templates_dict = get_all_templates(module, auth) found = 0 found_template = None template_name = '' if "DOCUMENT_POOL" in all_templates_dict and "DOCUMENT" in all_templates_dict["DOCUMENT_POOL"]: for template in all_templates_dict["DOCUMENT_POOL"]["DOCUMENT"]: if pred(template): found = found + 1 found_template = template template_name = template["NAME"] if found <= 0: return None elif found > 1: module.fail_json(msg="There is no template with unique name: " + template_name) else: return found_template def get_all_services(module, auth): try: response = open_url(auth.url + "/service", method="GET", force_basic_auth=True, url_username=auth.user, url_password=auth.password) except Exception as e: module.fail_json(msg=str(e)) return module.from_json(response.read()) def get_service(module, auth, pred): all_services_dict = get_all_services(module, auth) found = 0 found_service = None service_name = '' if "DOCUMENT_POOL" in all_services_dict and "DOCUMENT" in all_services_dict["DOCUMENT_POOL"]: for service in all_services_dict["DOCUMENT_POOL"]["DOCUMENT"]: if pred(service): found = found + 1 found_service = service service_name = service["NAME"] # fail if there are more services with same name if found > 1: module.fail_json(msg="There are multiple services with a name: '" + service_name + "'. You have to use a unique service name or use 'service_id' instead.") elif found <= 0: return None else: return found_service def get_service_by_id(module, auth, service_id): return get_service(module, auth, lambda service: (int(service["ID"]) == int(service_id))) if service_id else None def get_service_by_name(module, auth, service_name): return get_service(module, auth, lambda service: (service["NAME"] == service_name)) def get_service_info(module, auth, service): result = { "service_id": int(service["ID"]), "service_name": service["NAME"], "group_id": int(service["GID"]), "group_name": service["GNAME"], "owner_id": int(service["UID"]), "owner_name": service["UNAME"], "state": STATES[service["TEMPLATE"]["BODY"]["state"]] } roles_status = service["TEMPLATE"]["BODY"]["roles"] roles = [] for role in roles_status: nodes_ids = [] if "nodes" in role: for node in role["nodes"]: nodes_ids.append(node["deploy_id"]) roles.append({"name": role["name"], "cardinality": role["cardinality"], "state": STATES[int(role["state"])], "ids": nodes_ids}) result["roles"] = roles result["mode"] = int(parse_service_permissions(service)) return result def create_service(module, auth, template_id, service_name, custom_attrs, unique, wait, wait_timeout): # make sure that the values in custom_attrs dict are strings custom_attrs_with_str = {k: str(v) for k, v in custom_attrs.items()} data = { "action": { "perform": "instantiate", "params": { "merge_template": { "custom_attrs_values": custom_attrs_with_str, "name": service_name } } } } try: response = open_url(auth.url + "/service_template/" + str(template_id) + "/action", method="POST", data=module.jsonify(data), force_basic_auth=True, url_username=auth.user, url_password=auth.password) except Exception as e: module.fail_json(msg=str(e)) service_result = module.from_json(response.read())["DOCUMENT"] return service_result def wait_for_service_to_become_ready(module, auth, service_id, wait_timeout): import time start_time = time.time() while (time.time() - start_time) < wait_timeout: try: status_result = open_url(auth.url + "/service/" + str(service_id), method="GET", force_basic_auth=True, url_username=auth.user, url_password=auth.password) except Exception as e: module.fail_json(msg="Request for service status has failed. Error message: " + str(e)) status_result = module.from_json(status_result.read()) service_state = status_result["DOCUMENT"]["TEMPLATE"]["BODY"]["state"] if service_state in [STATES.index("RUNNING"), STATES.index("COOLDOWN")]: return status_result["DOCUMENT"] elif service_state not in [STATES.index("PENDING"), STATES.index("DEPLOYING"), STATES.index("SCALING")]: log_message = '' for log_info in status_result["DOCUMENT"]["TEMPLATE"]["BODY"]["log"]: if log_info["severity"] == "E": log_message = log_message + log_info["message"] break module.fail_json(msg="Deploying is unsuccessful. Service state: " + STATES[service_state] + ". Error message: " + log_message) time.sleep(1) module.fail_json(msg="Wait timeout has expired") def change_service_permissions(module, auth, service_id, permissions): data = { "action": { "perform": "chmod", "params": {"octet": permissions} } } try: status_result = open_url(auth.url + "/service/" + str(service_id) + "/action", method="POST", force_basic_auth=True, url_username=auth.user, url_password=auth.password, data=module.jsonify(data)) except Exception as e: module.fail_json(msg=str(e)) def change_service_owner(module, auth, service_id, owner_id): data = { "action": { "perform": "chown", "params": {"owner_id": owner_id} } } try: status_result = open_url(auth.url + "/service/" + str(service_id) + "/action", method="POST", force_basic_auth=True, url_username=auth.user, url_password=auth.password, data=module.jsonify(data)) except Exception as e: module.fail_json(msg=str(e)) def change_service_group(module, auth, service_id, group_id): data = { "action": { "perform": "chgrp", "params": {"group_id": group_id} } } try: status_result = open_url(auth.url + "/service/" + str(service_id) + "/action", method="POST", force_basic_auth=True, url_username=auth.user, url_password=auth.password, data=module.jsonify(data)) except Exception as e: module.fail_json(msg=str(e)) def change_role_cardinality(module, auth, service_id, role, cardinality, force): data = { "cardinality": cardinality, "force": force } try: status_result = open_url(auth.url + "/service/" + str(service_id) + "/role/" + role, method="PUT", force_basic_auth=True, url_username=auth.user, url_password=auth.password, data=module.jsonify(data)) except Exception as e: module.fail_json(msg=str(e)) if status_result.getcode() != 204: module.fail_json(msg="Failed to change cardinality for role: " + role + ". Return code: " + str(status_result.getcode())) def check_change_service_owner(module, service, owner_id): old_owner_id = int(service["UID"]) return old_owner_id != owner_id def check_change_service_group(module, service, group_id): old_group_id = int(service["GID"]) return old_group_id != group_id def parse_service_permissions(service): perm_dict = service["PERMISSIONS"] ''' This is the structure of the 'PERMISSIONS' dictionary: "PERMISSIONS": { "OWNER_U": "1", "OWNER_M": "1", "OWNER_A": "0", "GROUP_U": "0", "GROUP_M": "0", "GROUP_A": "0", "OTHER_U": "0", "OTHER_M": "0", "OTHER_A": "0" } ''' owner_octal = int(perm_dict["OWNER_U"]) * 4 + int(perm_dict["OWNER_M"]) * 2 + int(perm_dict["OWNER_A"]) group_octal = int(perm_dict["GROUP_U"]) * 4 + int(perm_dict["GROUP_M"]) * 2 + int(perm_dict["GROUP_A"]) other_octal = int(perm_dict["OTHER_U"]) * 4 + int(perm_dict["OTHER_M"]) * 2 + int(perm_dict["OTHER_A"]) permissions = str(owner_octal) + str(group_octal) + str(other_octal) return permissions def check_change_service_permissions(module, service, permissions): old_permissions = parse_service_permissions(service) return old_permissions != permissions def check_change_role_cardinality(module, service, role_name, cardinality): roles_list = service["TEMPLATE"]["BODY"]["roles"] for role in roles_list: if role["name"] == role_name: return int(role["cardinality"]) != cardinality module.fail_json(msg="There is no role with name: " + role_name) def create_service_and_operation(module, auth, template_id, service_name, owner_id, group_id, permissions, custom_attrs, unique, wait, wait_timeout): if not service_name: service_name = '' changed = False service = None if unique: service = get_service_by_name(module, auth, service_name) if not service: if not module.check_mode: service = create_service(module, auth, template_id, service_name, custom_attrs, unique, wait, wait_timeout) changed = True # if check_mode=true and there would be changes, service doesn't exist and we can not get it if module.check_mode and changed: return {"changed": True} result = service_operation(module, auth, owner_id=owner_id, group_id=group_id, wait=wait, wait_timeout=wait_timeout, permissions=permissions, service=service) if result["changed"]: changed = True result["changed"] = changed return result def service_operation(module, auth, service_id=None, owner_id=None, group_id=None, permissions=None, role=None, cardinality=None, force=None, wait=False, wait_timeout=None, service=None): changed = False if not service: service = get_service_by_id(module, auth, service_id) else: service_id = service["ID"] if not service: module.fail_json(msg="There is no service with id: " + str(service_id)) if owner_id: if check_change_service_owner(module, service, owner_id): if not module.check_mode: change_service_owner(module, auth, service_id, owner_id) changed = True if group_id: if check_change_service_group(module, service, group_id): if not module.check_mode: change_service_group(module, auth, service_id, group_id) changed = True if permissions: if check_change_service_permissions(module, service, permissions): if not module.check_mode: change_service_permissions(module, auth, service_id, permissions) changed = True if role: if check_change_role_cardinality(module, service, role, cardinality): if not module.check_mode: change_role_cardinality(module, auth, service_id, role, cardinality, force) changed = True if wait and not module.check_mode: service = wait_for_service_to_become_ready(module, auth, service_id, wait_timeout) # if something has changed, fetch service info again if changed: service = get_service_by_id(module, auth, service_id) service_info = get_service_info(module, auth, service) service_info["changed"] = changed return service_info def delete_service(module, auth, service_id): service = get_service_by_id(module, auth, service_id) if not service: return {"changed": False} service_info = get_service_info(module, auth, service) service_info["changed"] = True if module.check_mode: return service_info try: result = open_url(auth.url + '/service/' + str(service_id), method="DELETE", force_basic_auth=True, url_username=auth.user, url_password=auth.password) except Exception as e: module.fail_json(msg="Service deletion has failed. Error message: " + str(e)) return service_info def get_template_by_name(module, auth, template_name): return get_template(module, auth, lambda template: (template["NAME"] == template_name)) def get_template_by_id(module, auth, template_id): return get_template(module, auth, lambda template: (int(template["ID"]) == int(template_id))) if template_id else None def get_template_id(module, auth, requested_id, requested_name): template = get_template_by_id(module, auth, requested_id) if requested_id else get_template_by_name(module, auth, requested_name) if template: return template["ID"] return None def get_service_id_by_name(module, auth, service_name): service = get_service_by_name(module, auth, service_name) if service: return service["ID"] return None 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('ONEFLOW_URL') if not username: username = os.environ.get('ONEFLOW_USERNAME') if not password: password = os.environ.get('ONEFLOW_PASSWORD') if not (url and username and password): module.fail_json(msg="One or more connection parameters (api_url, api_username, api_password) were not specified") from collections import namedtuple auth_params = namedtuple('auth', ('url', 'user', 'password')) return auth_params(url=url, user=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}, "service_name": {"required": False, "type": "str"}, "service_id": {"required": False, "type": "int"}, "template_name": {"required": False, "type": "str"}, "template_id": {"required": False, "type": "int"}, "state": { "default": "present", "choices": ['present', 'absent'], "type": "str" }, "mode": {"required": False, "type": "str"}, "owner_id": {"required": False, "type": "int"}, "group_id": {"required": False, "type": "int"}, "unique": {"default": False, "type": "bool"}, "wait": {"default": False, "type": "bool"}, "wait_timeout": {"default": 300, "type": "int"}, "custom_attrs": {"default": {}, "type": "dict"}, "role": {"required": False, "type": "str"}, "cardinality": {"required": False, "type": "int"}, "force": {"default": False, "type": "bool"} } module = AnsibleModule(argument_spec=fields, mutually_exclusive=[ ['template_id', 'template_name', 'service_id'], ['service_id', 'service_name'], ['template_id', 'template_name', 'role'], ['template_id', 'template_name', 'cardinality'], ['service_id', 'custom_attrs'] ], required_together=[['role', 'cardinality']], supports_check_mode=True) auth = get_connection_info(module) params = module.params service_name = params.get('service_name') service_id = params.get('service_id') requested_template_id = params.get('template_id') requested_template_name = params.get('template_name') state = params.get('state') permissions = params.get('mode') owner_id = params.get('owner_id') group_id = params.get('group_id') unique = params.get('unique') wait = params.get('wait') wait_timeout = params.get('wait_timeout') custom_attrs = params.get('custom_attrs') role = params.get('role') cardinality = params.get('cardinality') force = params.get('force') template_id = None if requested_template_id or requested_template_name: template_id = get_template_id(module, auth, requested_template_id, requested_template_name) if not template_id: if requested_template_id: 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) if unique and not service_name: module.fail_json(msg="You cannot use unique without passing service_name!") if template_id and state == 'absent': module.fail_json(msg="State absent is not valid for template") if template_id and state == 'present': # Instantiate a service result = create_service_and_operation(module, auth, template_id, service_name, owner_id, group_id, permissions, custom_attrs, unique, wait, wait_timeout) else: if not (service_id or service_name): module.fail_json(msg="To manage the service at least the service id or service name should be specified!") if custom_attrs: module.fail_json(msg="You can only set custom_attrs when instantiate service!") if not service_id: service_id = get_service_id_by_name(module, auth, service_name) # The task should be failed when we want to manage a non-existent service identified by its name if not service_id and state == 'present': module.fail_json(msg="There is no service with name: " + service_name) if state == 'absent': result = delete_service(module, auth, service_id) else: result = service_operation(module, auth, service_id, owner_id, group_id, permissions, role, cardinality, force, wait, wait_timeout) module.exit_json(**result) if __name__ == '__main__': main()