From dd953dbe960b118c43ec200f9f06b86386c0079a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 28 Aug 2018 19:55:21 +0200 Subject: cs_template: implement update and revamp (#37015) --- .../modules/cloud/cloudstack/cs_template.py | 360 +++++++++++++-------- 1 file changed, 234 insertions(+), 126 deletions(-) (limited to 'lib/ansible/modules/cloud/cloudstack') diff --git a/lib/ansible/modules/cloud/cloudstack/cs_template.py b/lib/ansible/modules/cloud/cloudstack/cs_template.py index b107ca4f4e..f29870436d 100644 --- a/lib/ansible/modules/cloud/cloudstack/cs_template.py +++ b/lib/ansible/modules/cloud/cloudstack/cs_template.py @@ -1,22 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # -# (c) 2015, René Moser -# -# 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 . +# Copyright (c) 2015, René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['stableinterface'], @@ -28,7 +14,9 @@ DOCUMENTATION = ''' module: cs_template short_description: Manages templates on Apache CloudStack based clouds. description: - - Register a template from URL, create a template from a ROOT volume of a stopped VM or its snapshot, extract and delete templates. + - Register templates from an URL. + - Create templates from a ROOT volume of a stopped VM or its snapshot. + - Update (since version 2.7), extract and delete templates. version_added: '2.0' author: "René Moser (@resmo)" options: @@ -38,8 +26,8 @@ options: required: true url: description: - - URL of where the template is hosted on C(state=present). - - URL to which the template would be extracted on C(state=extracted). + - URL of where the template is hosted on I(state=present). + - URL to which the template would be extracted on I(state=extracted). - Mutually exclusive with C(vm). vm: description: @@ -57,38 +45,36 @@ options: description: - The MD5 checksum value of this template. - If set, we search by checksum instead of name. - default: false is_ready: description: - - This flag is used for searching existing templates. - - If set to C(true), it will only list template ready for deployment e.g. successfully downloaded and installed. - - Recommended to set it to C(false). - default: false + - "Note: this flag was not implemented and therefore marked as deprecated." + - Deprecated, will be removed in version 2.11. + type: bool is_public: description: - Register the template to be publicly available to all users. - Only used if C(state) is present. - default: false + type: bool is_featured: description: - Register the template to be featured. - Only used if C(state) is present. - default: false + type: bool is_dynamically_scalable: description: - Register the template having XS/VMWare tools installed in order to support dynamic scaling of VM CPU/memory. - Only used if C(state) is present. - default: false + type: bool cross_zones: description: - Whether the template should be synced or removed across zones. - Only used if C(state) is present or absent. - default: false + default: no + type: bool mode: description: - Mode for the template extraction. - - Only used if C(state=extracted). - required: false + - Only used if I(state=extracted). default: http_download choices: [ http_download, ftp_upload ] domain: @@ -107,63 +93,92 @@ options: template_filter: description: - Name of the filter used to search for the template. - required: false + - The filter C(all) was added in 2.7. default: self - choices: [ featured, self, selfexecutable, sharedexecutable, executable, community ] + choices: [ all, featured, self, selfexecutable, sharedexecutable, executable, community ] + template_find_options: + description: + - Options to find a template uniquely. + - More than one allowed. + choices: [ display_text, checksum, cross_zones ] + version_added: 2.7 + aliases: [ template_find_option ] + default: [] hypervisor: description: - Name the hypervisor to be used for creating the new template. - - Relevant when using C(state=present). - choices: [ KVM, VMware, BareMetal, XenServer, LXC, HyperV, UCS, OVM, Simulator ] + - Relevant when using I(state=present). + choices: + - KVM + - kvm + - VMware + - vmware + - BareMetal + - baremetal + - XenServer + - xenserver + - LXC + - lxc + - HyperV + - hyperv + - UCS + - ucs + - OVM + - ovm + - Simulator + - simulator requires_hvm: description: - - true if this template requires HVM. - default: false + - Whether the template requires HVM or not. + - Only considered while creating the template. + type: bool password_enabled: description: - - True if the template supports the password reset feature. - default: false + - Enable template password reset support. + type: bool template_tag: description: - - the tag for this template. + - The tag for this template. sshkey_enabled: description: - True if the template supports the sshkey upload feature. - default: false + - Only considered if C(url) is used (API limitation). + type: bool is_routing: description: - - True if the template type is routing i.e., if template is used to deploy router. + - Sets the template type to routing, i.e. if template is used to deploy routers. - Only considered if C(url) is used. + type: bool format: description: - The format for the template. - - Relevant when using C(state=present). + - Only considered if I(state=present). choices: [ QCOW2, RAW, VHD, OVA ] is_extractable: description: - - True if the template or its derivatives are extractable. - default: false + - Allows the template or its derivatives to be extractable. + type: bool details: description: - Template details in key/value pairs. bits: description: - 32 or 64 bits support. - required: false default: 64 + choices: [ 32, 64 ] display_text: description: - Display text of the template. state: description: - State of the template. - required: false default: present choices: [ present, absent, extracted ] poll_async: description: - Poll async jobs until job has finished. - default: true + default: yes + type: bool tags: description: - List of tags. Tags are a list of dictionaries having keys C(key) and C(value). @@ -184,12 +199,25 @@ EXAMPLES = ''' cross_zones: yes os_type: Debian GNU/Linux 7(64-bit) -- name: create a template from a stopped virtual machine's volume +- name: Create a template from a stopped virtual machine's volume local_action: module: cs_template - name: debian-base-template - vm: debian-base-vm - os_type: Debian GNU/Linux 7(64-bit) + name: Debian 9 (64-bit) 20GB ({{ ansible_date_time.date }}) + vm: debian-9-base-vm + os_type: Debian GNU/Linux 9 (64-bit) + zone: tokio-ix + password_enabled: yes + is_public: yes + +# Note: Use template_find_option(s) when a template name is not unique +- name: Create a template from a stopped virtual machine's volume + local_action: + module: cs_template + name: Debian 9 (64-bit) + display_text: Debian 9 (64-bit) 20GB ({{ ansible_date_time.date }}) + template_find_option: display_text + vm: debian-9-base-vm + os_type: Debian GNU/Linux 9 (64-bit) zone: tokio-ix password_enabled: yes is_public: yes @@ -197,10 +225,9 @@ EXAMPLES = ''' - name: create a template from a virtual machine's root volume snapshot local_action: module: cs_template - name: debian-base-template - vm: debian-base-vm + name: Debian 9 (64-bit) Snapshot ROOT-233_2015061509114 snapshot: ROOT-233_2015061509114 - os_type: Debian GNU/Linux 7(64-bit) + os_type: Debian GNU/Linux 9 (64-bit) zone: tokio-ix password_enabled: yes is_public: yes @@ -370,9 +397,6 @@ class AnsibleCloudStackTemplate(AnsibleCloudStack): 'tempaltetype': 'template_type', 'ostypename': 'os_type', 'crossZones': 'cross_zones', - 'isextractable': 'is_extractable', - 'isfeatured': 'is_featured', - 'ispublic': 'is_public', 'format': 'format', 'hypervisor': 'hypervisor', 'url': 'url', @@ -432,24 +456,36 @@ class AnsibleCloudStackTemplate(AnsibleCloudStack): return self._get_by_key(key, s) self.module.fail_json(msg="Snapshot '%s' not found" % snapshot) - def create_template(self): + def present_template(self): template = self.get_template() - if not template: - self.result['changed'] = True + if template: + template = self.update_template(template) + elif self.module.params.get('url'): + template = self.register_template() + elif self.module.params.get('vm'): + template = self.create_template() + else: + self.fail_json(msg="one of the following is required on state=present: url, vm") + return template - args = self._get_args() - snapshot_id = self.get_snapshot(key='id') - if snapshot_id: - args['snapshotid'] = snapshot_id - else: - args['volumeid'] = self.get_root_volume('id') + def create_template(self): + template = None + self.result['changed'] = True - if not self.module.check_mode: - template = self.query_api('createTemplate', **args) + args = self._get_args() + snapshot_id = self.get_snapshot(key='id') + if snapshot_id: + args['snapshotid'] = snapshot_id + else: + args['volumeid'] = self.get_root_volume('id') + + if not self.module.check_mode: + template = self.query_api('createTemplate', **args) + + poll_async = self.module.params.get('poll_async') + if poll_async: + template = self.poll_job(template, 'template') - poll_async = self.module.params.get('poll_async') - if poll_async: - template = self.poll_job(template, 'template') if template: template = self.ensure_tags(resource=template, resource_type='Template') @@ -462,59 +498,128 @@ class AnsibleCloudStackTemplate(AnsibleCloudStack): 'hypervisor', ] self.module.fail_on_missing_params(required_params=required_params) - template = self.get_template() - if not template: - self.result['changed'] = True - args = self._get_args() - args.update({ - 'url': self.module.params.get('url'), - 'format': self.module.params.get('format'), - 'checksum': self.module.params.get('checksum'), - 'isextractable': self.module.params.get('is_extractable'), - 'isrouting': self.module.params.get('is_routing'), - 'sshkeyenabled': self.module.params.get('sshkey_enabled'), - 'hypervisor': self.get_hypervisor(), - 'domainid': self.get_domain(key='id'), - 'account': self.get_account(key='name'), - 'projectid': self.get_project(key='id'), - }) + template = None + self.result['changed'] = True + args = self._get_args() + args.update({ + 'url': self.module.params.get('url'), + 'format': self.module.params.get('format'), + 'checksum': self.module.params.get('checksum'), + 'isextractable': self.module.params.get('is_extractable'), + 'isrouting': self.module.params.get('is_routing'), + 'sshkeyenabled': self.module.params.get('sshkey_enabled'), + 'hypervisor': self.get_hypervisor(), + 'domainid': self.get_domain(key='id'), + 'account': self.get_account(key='name'), + 'projectid': self.get_project(key='id'), + }) - if not self.module.params.get('cross_zones'): - args['zoneid'] = self.get_zone(key='id') - else: - args['zoneid'] = -1 + if not self.module.params.get('cross_zones'): + args['zoneid'] = self.get_zone(key='id') + else: + args['zoneid'] = -1 + + if not self.module.check_mode: + self.query_api('registerTemplate', **args) + template = self.get_template() + return template + + def update_template(self, template): + args = { + 'id': template['id'], + 'displaytext': self.get_or_fallback('display_text', 'name'), + 'format': self.module.params.get('format'), + 'isdynamicallyscalable': self.module.params.get('is_dynamically_scalable'), + 'isrouting': self.module.params.get('is_routing'), + 'ostypeid': self.get_os_type(key='id'), + 'passwordenabled': self.module.params.get('password_enabled'), + } + if self.has_changed(args, template): + self.result['changed'] = True + if not self.module.check_mode: + self.query_api('updateTemplate', **args) + template = self.get_template() + args = { + 'id': template['id'], + 'isextractable': self.module.params.get('is_extractable'), + 'isfeatured': self.module.params.get('is_featured'), + 'ispublic': self.module.params.get('is_public'), + } + if self.has_changed(args, template): + self.result['changed'] = True if not self.module.check_mode: - res = self.query_api('registerTemplate', **args) - template = res['template'] + self.query_api('updateTemplatePermissions', **args) + # Refresh + template = self.get_template() + + if template: + template = self.ensure_tags(resource=template, resource_type='Template') + return template + def _is_find_option(self, param_name): + return param_name in self.module.params.get('template_find_options') + + def _find_option_match(self, template, param_name, internal_name=None): + if not internal_name: + internal_name = param_name + + if param_name in self.module.params.get('template_find_options'): + param_value = self.module.params.get(param_name) + + if not param_value: + self.fail_json(msg="The param template_find_options has %s but param was not provided." % param_name) + + if template[internal_name] == param_value: + return True + return False + def get_template(self): args = { - 'isready': self.module.params.get('is_ready'), + 'name': self.module.params.get('name'), 'templatefilter': self.module.params.get('template_filter'), 'domainid': self.get_domain(key='id'), 'account': self.get_account(key='name'), 'projectid': self.get_project(key='id') } - if not self.module.params.get('cross_zones'): + + cross_zones = self.module.params.get('cross_zones') + if not cross_zones: args['zoneid'] = self.get_zone(key='id') - # if checksum is set, we only look on that. - checksum = self.module.params.get('checksum') - if not checksum: - args['name'] = self.module.params.get('name') + template_found = None templates = self.query_api('listTemplates', **args) if templates: - # if checksum is set, we only look on that. - if not checksum: - return templates['template'][0] - else: - for i in templates['template']: - if 'checksum' in i and i['checksum'] == checksum: - return i - return None + for tmpl in templates['template']: + + if self._is_find_option('cross_zones') and not self._find_option_match( + template=tmpl, + param_name='cross_zones', + internal_name='crossZones'): + continue + + if self._is_find_option('checksum') and not self._find_option_match( + template=tmpl, + param_name='checksum'): + continue + + if self._is_find_option('display_text') and not self._find_option_match( + template=tmpl, + param_name='display_text', + internal_name='displaytext'): + continue + + if not template_found: + template_found = tmpl + # A cross zones template has one entry per zone but the same id + elif tmpl['id'] == template_found['id']: + continue + else: + self.fail_json(msg="Multiple templates found matching provided params. Please use template_find_options.") + + return template_found def extract_template(self): template = self.get_template() @@ -556,6 +661,14 @@ class AnsibleCloudStackTemplate(AnsibleCloudStack): res = self.poll_job(res, 'template') return template + def get_result(self, template): + super(AnsibleCloudStackTemplate, self).get_result(template) + if template: + self.result['is_extractable'] = True if template['isextractable'] else False + self.result['is_featured'] = True if template['isfeatured'] else False + self.result['is_public'] = True if template['ispublic'] else False + return self.result + def main(): argument_spec = cs_argument_spec() @@ -566,20 +679,21 @@ def main(): vm=dict(), snapshot=dict(), os_type=dict(), - is_ready=dict(type='bool', default=False), - is_public=dict(type='bool', default=True), - is_featured=dict(type='bool', default=False), - is_dynamically_scalable=dict(type='bool', default=False), - is_extractable=dict(type='bool', default=False), + is_ready=dict(type='bool', removed_in_version='2.11'), + is_public=dict(type='bool'), + is_featured=dict(type='bool'), + is_dynamically_scalable=dict(type='bool'), + is_extractable=dict(type='bool'), is_routing=dict(type='bool'), checksum=dict(), - template_filter=dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), + template_filter=dict(default='self', choices=['all', 'featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), + template_find_options=dict(type='list', choices=['display_text', 'checksum', 'cross_zones'], aliases=['template_find_option'], default=[]), hypervisor=dict(choices=CS_HYPERVISORS), - requires_hvm=dict(type='bool', default=False), - password_enabled=dict(type='bool', default=False), + requires_hvm=dict(type='bool'), + password_enabled=dict(type='bool'), template_tag=dict(), - sshkey_enabled=dict(type='bool', default=False), - format=dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA'], ), + sshkey_enabled=dict(type='bool'), + format=dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA']), details=dict(), bits=dict(type='int', choices=[32, 64], default=64), state=dict(choices=['present', 'absent', 'extracted'], default='present'), @@ -606,19 +720,13 @@ def main(): acs_tpl = AnsibleCloudStackTemplate(module) state = module.params.get('state') - if state in ['absent']: + if state == 'absent': tpl = acs_tpl.remove_template() - elif state in ['extracted']: + elif state == 'extracted': tpl = acs_tpl.extract_template() - else: - if module.params.get('url'): - tpl = acs_tpl.register_template() - elif module.params.get('vm'): - tpl = acs_tpl.create_template() - else: - module.fail_json(msg="one of the following is required on state=present: url,vm") + tpl = acs_tpl.present_template() result = acs_tpl.get_result(tpl) -- cgit v1.2.1