diff --git a/plugins/module_utils/opennebula.py b/plugins/module_utils/opennebula.py index 0b95c6185b..a0a8d1305b 100644 --- a/plugins/module_utils/opennebula.py +++ b/plugins/module_utils/opennebula.py @@ -39,14 +39,16 @@ class OpenNebulaModule: wait_timeout=dict(type='int', default=300), ) - def __init__(self, argument_spec, supports_check_mode=False, mutually_exclusive=None): + def __init__(self, argument_spec, supports_check_mode=False, mutually_exclusive=None, required_one_of=None, required_if=None): - module_args = OpenNebulaModule.common_args + module_args = OpenNebulaModule.common_args.copy() module_args.update(argument_spec) self.module = AnsibleModule(argument_spec=module_args, supports_check_mode=supports_check_mode, - mutually_exclusive=mutually_exclusive) + mutually_exclusive=mutually_exclusive, + required_one_of=required_one_of, + required_if=required_if) self.result = dict(changed=False, original_message='', message='') diff --git a/plugins/modules/cloud/opennebula/one_template.py b/plugins/modules/cloud/opennebula/one_template.py new file mode 100644 index 0000000000..b4c8a2fa83 --- /dev/null +++ b/plugins/modules/cloud/opennebula/one_template.py @@ -0,0 +1,276 @@ +#!/usr/bin/python +# +# Copyright: (c) 2021, Georg Gadinger +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: one_template + +short_description: Manages OpenNebula templates + +version_added: 2.4.0 + +requirements: + - pyone + +description: + - "Manages OpenNebula templates." + +options: + id: + description: + - A I(id) of the template you would like to manage. If not set then a + - new template will be created with the given I(name). + type: int + name: + description: + - A I(name) of the template you would like to manage. If a template with + - the given name does not exist it will be created, otherwise it will be + - managed by this module. + type: str + template: + description: + - A string containing the template contents. + type: str + state: + description: + - C(present) - state that is used to manage the template. + - C(absent) - delete the template. + choices: ["present", "absent"] + default: present + type: str + +notes: + - Supports C(check_mode). Note that check mode always returns C(changed=true) for existing templates, even if the template would not actually change. + +extends_documentation_fragment: + - community.general.opennebula + +author: + - "Georg Gadinger (@nilsding)" +''' + +EXAMPLES = ''' +- name: Fetch the TEMPLATE by id + community.general.one_template: + id: 6459 + register: result + +- name: Print the TEMPLATE properties + ansible.builtin.debug: + var: result + +- name: Fetch the TEMPLATE by name + community.general.one_template: + name: tf-prd-users-workerredis-p6379a + register: result + +- name: Create a new or update an existing TEMPLATE + community.general.one_template: + name: generic-opensuse + template: | + CONTEXT = [ + HOSTNAME = "generic-opensuse" + ] + CPU = "1" + CUSTOM_ATTRIBUTE = "" + DISK = [ + CACHE = "writeback", + DEV_PREFIX = "sd", + DISCARD = "unmap", + IMAGE = "opensuse-leap-15.2", + IMAGE_UNAME = "oneadmin", + IO = "threads", + SIZE = "" ] + MEMORY = "2048" + NIC = [ + MODEL = "virtio", + NETWORK = "testnet", + NETWORK_UNAME = "oneadmin" ] + OS = [ + ARCH = "x86_64", + BOOT = "disk0" ] + SCHED_REQUIREMENTS = "CLUSTER_ID=\\"100\\"" + VCPU = "2" + +- name: Delete the TEMPLATE by id + community.general.one_template: + id: 6459 + state: absent +''' + +RETURN = ''' +id: + description: template id + type: int + returned: when I(state=present) + sample: 153 +name: + description: template name + type: str + returned: when I(state=present) + sample: app1 +template: + description: the parsed template + type: dict + returned: when I(state=present) +group_id: + description: template's group id + type: int + returned: when I(state=present) + sample: 1 +group_name: + description: template's group name + type: str + returned: when I(state=present) + sample: one-users +owner_id: + description: template's owner id + type: int + returned: when I(state=present) + sample: 143 +owner_name: + description: template's owner name + type: str + returned: when I(state=present) + sample: ansible-test +''' + + +from ansible_collections.community.general.plugins.module_utils.opennebula import OpenNebulaModule + + +class TemplateModule(OpenNebulaModule): + def __init__(self): + argument_spec = dict( + id=dict(type='int', required=False), + name=dict(type='str', required=False), + state=dict(type='str', choices=['present', 'absent'], default='present'), + template=dict(type='str', required=False), + ) + + mutually_exclusive = [ + ['id', 'name'] + ] + + required_one_of = [('id', 'name')] + + required_if = [ + ['state', 'present', ['template']] + ] + + OpenNebulaModule.__init__(self, + argument_spec, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + required_one_of=required_one_of, + required_if=required_if) + + def run(self, one, module, result): + params = module.params + id = params.get('id') + name = params.get('name') + desired_state = params.get('state') + template_data = params.get('template') + + self.result = {} + + template = self.get_template_instance(id, name) + needs_creation = False + if not template and desired_state != 'absent': + if id: + module.fail_json(msg="There is no template with id=" + str(id)) + else: + needs_creation = True + + if desired_state == 'absent': + self.result = self.delete_template(template) + else: + if needs_creation: + self.result = self.create_template(name, template_data) + else: + self.result = self.update_template(template, template_data) + + self.exit() + + def get_template(self, predicate): + # -3 means "Resources belonging to the user" + # the other two parameters are used for pagination, -1 for both essentially means "return all" + pool = self.one.templatepool.info(-3, -1, -1) + + for template in pool.VMTEMPLATE: + if predicate(template): + return template + + return None + + def get_template_by_id(self, template_id): + return self.get_template(lambda template: (template.ID == template_id)) + + def get_template_by_name(self, template_name): + return self.get_template(lambda template: (template.NAME == template_name)) + + def get_template_instance(self, requested_id, requested_name): + if requested_id: + return self.get_template_by_id(requested_id) + else: + return self.get_template_by_name(requested_name) + + def get_template_info(self, template): + info = { + 'id': template.ID, + 'name': template.NAME, + 'template': template.TEMPLATE, + 'user_name': template.UNAME, + 'user_id': template.UID, + 'group_name': template.GNAME, + 'group_id': template.GID, + } + + return info + + def create_template(self, name, template_data): + if not self.module.check_mode: + self.one.template.allocate("NAME = \"" + name + "\"\n" + template_data) + + result = self.get_template_info(self.get_template_by_name(name)) + result['changed'] = True + + return result + + def update_template(self, template, template_data): + if not self.module.check_mode: + # 0 = replace the whole template + self.one.template.update(template.ID, template_data, 0) + + result = self.get_template_info(self.get_template_by_id(template.ID)) + if self.module.check_mode: + # Unfortunately it is not easy to detect if the template would have changed, therefore always report a change here. + result['changed'] = True + else: + # if the previous parsed template data is not equal to the updated one, this has changed + result['changed'] = template.TEMPLATE != result['template'] + + return result + + def delete_template(self, template): + if not template: + return {'changed': False} + + if not self.module.check_mode: + self.one.template.delete(template.ID) + + return {'changed': True} + + +def main(): + TemplateModule().run_module() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/one_template.py b/plugins/modules/one_template.py new file mode 120000 index 0000000000..78637e1843 --- /dev/null +++ b/plugins/modules/one_template.py @@ -0,0 +1 @@ +./cloud/opennebula/one_template.py \ No newline at end of file diff --git a/tests/integration/targets/one_template/aliases b/tests/integration/targets/one_template/aliases new file mode 100644 index 0000000000..1ff4e0b13e --- /dev/null +++ b/tests/integration/targets/one_template/aliases @@ -0,0 +1,2 @@ +cloud/opennebula +shippable/cloud/group1 diff --git a/tests/integration/targets/one_template/files/testhost/tmp/opennebula-fixtures.json.gz b/tests/integration/targets/one_template/files/testhost/tmp/opennebula-fixtures.json.gz new file mode 100644 index 0000000000..169451a22d Binary files /dev/null and b/tests/integration/targets/one_template/files/testhost/tmp/opennebula-fixtures.json.gz differ diff --git a/tests/integration/targets/one_template/tasks/main.yml b/tests/integration/targets/one_template/tasks/main.yml new file mode 100644 index 0000000000..fb60e5a98c --- /dev/null +++ b/tests/integration/targets/one_template/tasks/main.yml @@ -0,0 +1,243 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# test code for the one_template module + + +# ENVIRONMENT PREPARATION + +- name: "copy fixtures to test host" + copy: + src: testhost/tmp/opennebula-fixtures.json.gz + dest: /tmp + when: + - opennebula_test_fixture + - opennebula_test_fixture_replay + + +# Create a new template + +- name: "Create a new TEMPLATE" + one_template: + api_url: "{{ opennebula_url }}" + api_username: "{{ opennebula_username }}" + api_password: "{{ opennebula_password }}" + name: ansible-onetemplate-test + template: | + CONTEXT = [ + HOSTNAME = "ansible-onetemplate", + NETWORK = "YES", + SSH_PUBLIC_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKAQwTkU84eEnhX3r60Mn5TPh99BDxyCNJu12OB5sfMu foxy@FoxPad", + USERNAME = "root" ] + CPU = "1" + CUSTOM_ATTRIBUTE = "" + DISK = [ + CACHE = "writeback", + DEV_PREFIX = "sd", + DISCARD = "unmap", + IMAGE = "ansible-onetemplate", + IMAGE_UNAME = "oneadmin", + IO = "threads", + SIZE = "" ] + FEATURES = [ + VIRTIO_SCSI_QUEUES = "2" ] + GRAPHICS = [ + KEYMAP = "de", + LISTEN = "0.0.0.0", + TYPE = "VNC" ] + MEMORY = "2048" + NIC = [ + MODEL = "virtio", + NETWORK = "tf-prd-centos", + NETWORK_UNAME = "oneadmin" ] + OS = [ + ARCH = "x86_64", + BOOT = "disk0" ] + SCHED_REQUIREMENTS = "CLUSTER_ID=\"100\"" + VCPU = "2" + environment: + PYONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}" + PYONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz + PYONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}" + PYONE_TEST_FIXTURE_UNIT: test_create_template + register: result + +- name: "assert that creation worked" + assert: + that: + - result is changed + + +# Updating a template + +- name: "Update an existing TEMPLATE" + one_template: + api_url: "{{ opennebula_url }}" + api_username: "{{ opennebula_username }}" + api_password: "{{ opennebula_password }}" + name: ansible-onetemplate-test + template: | + CONTEXT = [ + HOSTNAME = "ansible-onetemplate", + NETWORK = "YES", + SSH_PUBLIC_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKAQwTkU84eEnhX3r60Mn5TPh99BDxyCNJu12OB5sfMu foxy@FoxPad", + USERNAME = "root" ] + CPU = "1" + CUSTOM_ATTRIBUTE = "" + DISK = [ + CACHE = "writeback", + DEV_PREFIX = "sd", + DISCARD = "unmap", + IMAGE = "ansible-onetemplate", + IMAGE_UNAME = "oneadmin", + IO = "threads", + SIZE = "" ] + FEATURES = [ + VIRTIO_SCSI_QUEUES = "2" ] + GRAPHICS = [ + KEYMAP = "de", + LISTEN = "0.0.0.0", + TYPE = "VNC" ] + MEMORY = "4096" + NIC = [ + MODEL = "virtio", + NETWORK = "tf-prd-centos", + NETWORK_UNAME = "oneadmin" ] + OS = [ + ARCH = "x86_64", + BOOT = "disk0" ] + SCHED_REQUIREMENTS = "CLUSTER_ID=\"100\"" + VCPU = "2" + environment: + PYONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}" + PYONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz + PYONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}" + PYONE_TEST_FIXTURE_UNIT: test_update_existing_template + register: result + +- name: "assert that it updated the template" + assert: + that: + - result is changed + +- name: "Update an existing TEMPLATE with the same changes again" + one_template: + api_url: "{{ opennebula_url }}" + api_username: "{{ opennebula_username }}" + api_password: "{{ opennebula_password }}" + name: ansible-onetemplate-test + template: | + CONTEXT = [ + HOSTNAME = "ansible-onetemplate", + NETWORK = "YES", + SSH_PUBLIC_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKAQwTkU84eEnhX3r60Mn5TPh99BDxyCNJu12OB5sfMu foxy@FoxPad", + USERNAME = "root" ] + CPU = "1" + CUSTOM_ATTRIBUTE = "" + DISK = [ + CACHE = "writeback", + DEV_PREFIX = "sd", + DISCARD = "unmap", + IMAGE = "ansible-onetemplate", + IMAGE_UNAME = "oneadmin", + IO = "threads", + SIZE = "" ] + FEATURES = [ + VIRTIO_SCSI_QUEUES = "2" ] + GRAPHICS = [ + KEYMAP = "de", + LISTEN = "0.0.0.0", + TYPE = "VNC" ] + MEMORY = "4096" + NIC = [ + MODEL = "virtio", + NETWORK = "tf-prd-centos", + NETWORK_UNAME = "oneadmin" ] + OS = [ + ARCH = "x86_64", + BOOT = "disk0" ] + SCHED_REQUIREMENTS = "CLUSTER_ID=\"100\"" + VCPU = "2" + environment: + PYONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}" + PYONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz + PYONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}" + PYONE_TEST_FIXTURE_UNIT: test_update_existing_and_already_updated_template + register: result + +- name: "assert that there was no change" + assert: + that: + - result is not changed + + +# Deletion of templates + +- name: "Delete a nonexisting TEMPLATE" + one_template: + api_url: "{{ opennebula_url }}" + api_username: "{{ opennebula_username }}" + api_password: "{{ opennebula_password }}" + name: ansible-onetemplate-test-nonexisting + state: absent + environment: + PYONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}" + PYONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz + PYONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}" + PYONE_TEST_FIXTURE_UNIT: test_delete_nonexisting_template + register: result + +- name: "assert that there was no change" + assert: + that: + - result is not changed + +- name: "Delete an existing TEMPLATE" + one_template: + api_url: "{{ opennebula_url }}" + api_username: "{{ opennebula_username }}" + api_password: "{{ opennebula_password }}" + name: ansible-onetemplate-test + state: absent + environment: + PYONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}" + PYONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz + PYONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}" + PYONE_TEST_FIXTURE_UNIT: test_delete_existing_template + register: result + +- name: "assert that there was a change" + assert: + that: + - result is changed + + +# Usage without `template` parameter + +- name: "Try to create use one_template with state=present and without the template parameter" + one_template: + api_url: "{{ opennebula_url }}" + api_username: "{{ opennebula_username }}" + api_password: "{{ opennebula_password }}" + name: ansible-onetemplate-test + state: present + register: result + ignore_errors: true + +- name: "assert that it failed because template is missing" + assert: + that: + - result is failed + + +# TEARDOWN + +- name: "fetch fixtures" + fetch: + src: /tmp/opennebula-fixtures.json.gz + dest: targets/one_host/files + when: + - opennebula_test_fixture + - not opennebula_test_fixture_replay