From c9d82024c7b5f461b44f504e51c996428ca7f274 Mon Sep 17 00:00:00 2001 From: Zim Kalinowski Date: Thu, 27 Jun 2019 08:15:55 +0800 Subject: [PATCH] shared image gallery modules (#57386) * modules to handle shared image gallery * update test * and firewall update * fixed gallery problems * fixed gallery version * fix * several fixes to the gallery * several fixes * fixes * fix non-updatable * fixed test & image * fixed idempotency * fix test * image version test fixed * fixes * changed ux * trigger * fix syntax * fixed sanity * updated module + test delete * fixed some sanity & delete * continue fixing sanity * sanity fix and pause after deleting image version * extended delay * removed sanity ignore * try to ignore errors * repeat until successful * more retries * updated test, etc. * updated test * updated shared image gallery docs --- .../module_utils/azure_rm_common_ext.py | 60 +- .../cloud/azure/azure_rm_azurefirewall.py | 3 +- .../modules/cloud/azure/azure_rm_gallery.py | 314 ++++++++++ .../cloud/azure/azure_rm_galleryimage.py | 554 ++++++++++++++++++ .../azure/azure_rm_galleryimageversion.py | 459 +++++++++++++++ lib/ansible/plugins/doc_fragments/azure.py | 1 + .../targets/azure_rm_gallery/aliases | 5 + .../targets/azure_rm_gallery/meta/main.yml | 2 + .../targets/azure_rm_gallery/tasks/main.yml | 263 +++++++++ test/sanity/validate-modules/ignore.txt | 1 - 10 files changed, 1635 insertions(+), 27 deletions(-) create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_gallery.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_galleryimage.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_galleryimageversion.py create mode 100644 test/integration/targets/azure_rm_gallery/aliases create mode 100644 test/integration/targets/azure_rm_gallery/meta/main.yml create mode 100644 test/integration/targets/azure_rm_gallery/tasks/main.yml diff --git a/lib/ansible/module_utils/azure_rm_common_ext.py b/lib/ansible/module_utils/azure_rm_common_ext.py index f12b4c9ce13..4935b2c2c1a 100644 --- a/lib/ansible/module_utils/azure_rm_common_ext.py +++ b/lib/ansible/module_utils/azure_rm_common_ext.py @@ -140,34 +140,44 @@ class AzureRMModuleBaseExt(AzureRMModuleBase): if new is None: return True elif isinstance(new, dict): + comparison_result = True if not isinstance(old, dict): - result['compare'] = 'changed [' + path + '] old dict is null' - return False - for k in new.keys(): - if not self.default_compare(modifiers, new.get(k), old.get(k, None), path + '/' + k, result): - return False - return True + result['compare'].append('changed [' + path + '] old dict is null') + comparison_result = False + else: + for k in set(new.keys()) | set(old.keys()): + new_item = new.get(k, None) + old_item = old.get(k, None) + if new_item is None: + if isinstance(old_item, dict): + new[k] = old_item + result['compare'].append('new item was empty, using old [' + path + '][ ' + k + ' ]') + elif not self.default_compare(modifiers, new_item, old_item, path + '/' + k, result): + comparison_result = False + return comparison_result elif isinstance(new, list): + comparison_result = True if not isinstance(old, list) or len(new) != len(old): - result['compare'] = 'changed [' + path + '] length is different or null' - return False - if isinstance(old[0], dict): - key = None - if 'id' in old[0] and 'id' in new[0]: - key = 'id' - elif 'name' in old[0] and 'name' in new[0]: - key = 'name' - else: - key = next(iter(old[0])) - new = sorted(new, key=lambda x: x.get(key, None)) - old = sorted(old, key=lambda x: x.get(key, None)) + result['compare'].append('changed [' + path + '] length is different or old value is null') + comparison_result = False else: - new = sorted(new) - old = sorted(old) - for i in range(len(new)): - if not self.default_compare(modifiers, new[i], old[i], path + '/*', result): - return False - return True + if isinstance(old[0], dict): + key = None + if 'id' in old[0] and 'id' in new[0]: + key = 'id' + elif 'name' in old[0] and 'name' in new[0]: + key = 'name' + else: + key = next(iter(old[0])) + new = sorted(new, key=lambda x: x.get(key, None)) + old = sorted(old, key=lambda x: x.get(key, None)) + else: + new = sorted(new) + old = sorted(old) + for i in range(len(new)): + if not self.default_compare(modifiers, new[i], old[i], path + '/*', result): + comparison_result = False + return comparison_result else: updatable = modifiers.get(path, {}).get('updatable', True) comparison = modifiers.get(path, {}).get('comparison', 'default') @@ -182,7 +192,7 @@ class AzureRMModuleBaseExt(AzureRMModuleBase): new = new.replace(' ', '').lower() old = old.replace(' ', '').lower() if str(new) != str(old): - result['compare'] = 'changed [' + path + '] ' + str(new) + ' != ' + str(old) + ' - ' + str(comparison) + result['compare'].append('changed [' + path + '] ' + str(new) + ' != ' + str(old) + ' - ' + str(comparison)) if updatable: return False else: diff --git a/lib/ansible/modules/cloud/azure/azure_rm_azurefirewall.py b/lib/ansible/modules/cloud/azure/azure_rm_azurefirewall.py index 225b7d04ab7..ec83fb2f466 100644 --- a/lib/ansible/modules/cloud/azure/azure_rm_azurefirewall.py +++ b/lib/ansible/modules/cloud/azure/azure_rm_azurefirewall.py @@ -19,7 +19,7 @@ module: azure_rm_azurefirewall version_added: '2.9' short_description: Manage Azure Firewall instance. description: - - 'Create, update and delete instance of Azure Firewall.' + - Create, update and delete instance of Azure Firewall. options: resource_group: description: @@ -600,6 +600,7 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): modifiers = {} self.create_compare_modifiers(self.module_arg_spec, '', modifiers) self.results['modifiers'] = modifiers + self.results['compare'] = [] if not self.default_compare(modifiers, self.body, old_response, '', self.results): self.to_do = Actions.Update diff --git a/lib/ansible/modules/cloud/azure/azure_rm_gallery.py b/lib/ansible/modules/cloud/azure/azure_rm_gallery.py new file mode 100644 index 00000000000..54dd22ef3af --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_gallery.py @@ -0,0 +1,314 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 Zim Kalinowski, (@zikalino) +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: azure_rm_gallery +version_added: '2.9' +short_description: Manage Azure Shared Image Gallery instance. +description: + - 'Create, update and delete instance of Azure Shared Image Gallery (SIG).' +options: + resource_group: + description: + - The name of the resource group. + required: true + type: str + name: + description: + - >- + The name of the Shared Image Gallery. + Valid names consist of less than 80 alphanumeric characters, underscores and periods. + required: true + type: str + location: + description: + - Resource location + type: str + description: + description: + - >- + The description of this Shared Image Gallery resource. This property is + updatable. + type: str + state: + description: + - Assert the state of the Gallery. + - >- + Use C(present) to create or update an Gallery and C(absent) to delete + it. + default: present + type: str + choices: + - absent + - present +extends_documentation_fragment: + - azure + - azure_tags +author: + - Zim Kalinowski (@zikalino) + +''' + +EXAMPLES = ''' +- name: Create or update a simple gallery. + azure_rm_gallery: + resource_group: myResourceGroup + name: myGallery1283 + location: West US + description: This is the gallery description. +''' + +RETURN = ''' +id: + description: + - Resource Id + returned: always + type: str + sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Compute/galleries/myGallery1283" +''' + +import time +import json +import re +from ansible.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt +from ansible.module_utils.azure_rm_common_rest import GenericRestClient +from copy import deepcopy +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class Actions: + NoAction, Create, Update, Delete = range(4) + + +class AzureRMGalleries(AzureRMModuleBaseExt): + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + updatable=False, + disposition='resourceGroupName', + required=True + ), + name=dict( + type='str', + updatable=False, + disposition='galleryName', + required=True + ), + location=dict( + type='str', + updatable=False, + disposition='/' + ), + description=dict( + type='str', + disposition='/properties/*' + ), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ) + ) + + self.resource_group = None + self.name = None + self.gallery = None + + self.results = dict(changed=False) + self.mgmt_client = None + self.state = None + self.url = None + self.status_code = [200, 201, 202] + self.to_do = Actions.NoAction + + self.body = {} + self.query_parameters = {} + self.query_parameters['api-version'] = '2019-03-01' + self.header_parameters = {} + self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' + + super(AzureRMGalleries, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=True) + + def exec_module(self, **kwargs): + for key in list(self.module_arg_spec.keys()): + if hasattr(self, key): + setattr(self, key, kwargs[key]) + elif kwargs[key] is not None: + self.body[key] = kwargs[key] + + self.inflate_parameters(self.module_arg_spec, self.body, 0) + + old_response = None + response = None + + self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, + base_url=self._cloud_environment.endpoints.resource_manager) + + resource_group = self.get_resource_group(self.resource_group) + + if 'location' not in self.body: + self.body['location'] = resource_group.location + + self.url = ('/subscriptions' + + '/{{ subscription_id }}' + + '/resourceGroups' + + '/{{ resource_group }}' + + '/providers' + + '/Microsoft.Compute' + + '/galleries' + + '/{{ gallery_name }}') + self.url = self.url.replace('{{ subscription_id }}', self.subscription_id) + self.url = self.url.replace('{{ resource_group }}', self.resource_group) + self.url = self.url.replace('{{ gallery_name }}', self.name) + + old_response = self.get_resource() + + if not old_response: + self.log("Gallery instance doesn't exist") + + if self.state == 'absent': + self.log("Old instance didn't exist") + else: + self.to_do = Actions.Create + else: + self.log('Gallery instance already exists') + + if self.state == 'absent': + self.to_do = Actions.Delete + else: + modifiers = {} + self.create_compare_modifiers(self.module_arg_spec, '', modifiers) + self.results['modifiers'] = modifiers + self.results['compare'] = [] + if not self.default_compare(modifiers, self.body, old_response, '', self.results): + self.to_do = Actions.Update + self.body['properties'].pop('identifier', None) + + if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): + self.log('Need to Create / Update the Gallery instance') + + if self.check_mode: + self.results['changed'] = True + return self.results + + response = self.create_update_resource() + + # if not old_response: + self.results['changed'] = True + # else: + # self.results['changed'] = old_response.__ne__(response) + self.log('Creation / Update done') + elif self.to_do == Actions.Delete: + self.log('Gallery instance deleted') + self.results['changed'] = True + + if self.check_mode: + return self.results + + self.delete_resource() + + # make sure instance is actually deleted, for some Azure resources, instance is hanging around + # for some time after deletion -- this should be really fixed in Azure + while self.get_resource(): + time.sleep(20) + else: + self.log('Gallery instance unchanged') + self.results['changed'] = False + response = old_response + + if response: + self.results["id"] = response["id"] + + return self.results + + def create_update_resource(self): + # self.log('Creating / Updating the Gallery instance {0}'.format(self.)) + + try: + response = self.mgmt_client.query(self.url, + 'PUT', + self.query_parameters, + self.header_parameters, + self.body, + self.status_code, + 600, + 30) + except CloudError as exc: + self.log('Error attempting to create the Gallery instance.') + self.fail('Error creating the Gallery instance: {0}'.format(str(exc))) + + try: + response = json.loads(response.text) + except Exception: + response = {'text': response.text} + pass + + return response + + def delete_resource(self): + # self.log('Deleting the Gallery instance {0}'.format(self.)) + try: + response = self.mgmt_client.query(self.url, + 'DELETE', + self.query_parameters, + self.header_parameters, + None, + self.status_code, + 600, + 30) + except CloudError as e: + self.log('Error attempting to delete the Gallery instance.') + self.fail('Error deleting the Gallery instance: {0}'.format(str(e))) + + return True + + def get_resource(self): + # self.log('Checking if the Gallery instance {0} is present'.format(self.)) + found = False + try: + response = self.mgmt_client.query(self.url, + 'GET', + self.query_parameters, + self.header_parameters, + None, + self.status_code, + 600, + 30) + response = json.loads(response.text) + found = True + self.log("Response : {0}".format(response)) + # self.log("AzureFirewall instance : {0} found".format(response.name)) + except CloudError as e: + self.log('Did not find the AzureFirewall instance.') + if found is True: + return response + + return False + + +def main(): + AzureRMGalleries() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_galleryimage.py b/lib/ansible/modules/cloud/azure/azure_rm_galleryimage.py new file mode 100644 index 00000000000..cb19efd2722 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_galleryimage.py @@ -0,0 +1,554 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 Zim Kalinowski, (@zikalino) +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: azure_rm_galleryimage +version_added: '2.9' +short_description: Manage Azure SIG Image instance. +description: + - 'Create, update and delete instance of Azure SIG Image.' +options: + resource_group: + description: + - The name of the resource group. + required: true + type: str + gallery_name: + description: + - >- + The name of the Shared Image Gallery in which the Image Definition is to + be created. + required: true + type: str + name: + description: + - >- + The name of the gallery Image Definition to be created or updated. The + allowed characters are alphabets and numbers with dots, dashes, and + periods allowed in the middle. The maximum length is 80 characters. + required: true + type: str + location: + description: + - Resource location + type: str + description: + description: + - >- + The description of this gallery Image Definition resource. This property + is updatable. + type: str + eula: + description: + - The Eula agreement for the gallery Image Definition. + type: str + privacy_statement_uri: + description: + - The privacy statement uri. + type: str + release_note_uri: + description: + - The release note uri. + type: str + os_type: + description: + - >- + This property allows you to specify the type of the OS that is included + in the disk when creating a VM from a managed image. + choices: + - windows + - linux + required: true + type: str + os_state: + description: + - The allowed values for OS State are 'Generalized'. + choices: + - generalized + - specialized + required: true + type: str + end_of_life_date: + description: + - >- + The end of life date of the gallery Image Definition. This property can + be used for decommissioning purposes. This property is updatable. + Format should be according to ISO-8601, for instance "2019-06-26". + type: str + identifier: + description: + - Image identifier. + required: true + type: dict + suboptions: + publisher: + description: + - The name of the gallery Image Definition publisher. + required: true + type: str + offer: + description: + - The name of the gallery Image Definition offer. + required: true + type: str + sku: + description: + - The name of the gallery Image Definition SKU. + required: true + type: str + recommended: + description: + - Recommended parameter values. + type: dict + suboptions: + v_cpus: + description: + - Number of virtual CPUs. + type: dict + suboptions: + min: + description: + - The minimum number of the resource. + type: int + max: + description: + - The maximum number of the resource. + type: int + memory: + description: + - Memory. + type: dict + suboptions: + min: + description: + - The minimum number of the resource. + type: int + max: + description: + - The maximum number of the resource. + type: int + disallowed: + description: + - Disalloved parameter values. + type: dict + suboptions: + disk_types: + description: + - A list of disallowed disk types. + type: list + purchase_plan: + description: + - Purchase plan. + type: dict + suboptions: + name: + description: + - The plan ID. + type: str + publisher: + description: + - The publisher ID. + type: str + product: + description: + - The product ID. + type: str + state: + description: + - Assert the state of the GalleryImage. + - >- + Use C(present) to create or update an GalleryImage and C(absent) to + delete it. + default: present + choices: + - absent + - present + type: str +extends_documentation_fragment: + - azure + - azure_tags +author: + - Zim Kalinowski (@zikalino) + +''' + +EXAMPLES = ''' +- name: Create or update gallery image + azure_rm_galleryimage: + resource_group: myResourceGroup + gallery_name: myGallery1283 + name: myImage + location: West US + os_type: linux + os_state: generalized + identifier: + publisher: myPublisherName + offer: myOfferName + sku: mySkuName +''' + +RETURN = ''' +id: + description: + - Resource Id + returned: always + type: str + sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Compute/galleries/myGalle + ry1283/images/myImage" +''' + +import time +import json +import re +from ansible.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt +from ansible.module_utils.azure_rm_common_rest import GenericRestClient +from copy import deepcopy +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class Actions: + NoAction, Create, Update, Delete = range(4) + + +class AzureRMGalleryImages(AzureRMModuleBaseExt): + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + updatable=False, + disposition='resourceGroupName', + required=True + ), + gallery_name=dict( + type='str', + updatable=False, + disposition='galleryName', + required=True + ), + name=dict( + type='str', + updatable=False, + disposition='galleryImageName', + required=True + ), + location=dict( + type='str', + updatable=False, + disposition='/' + ), + description=dict( + type='str', + disposition='/properties/*' + ), + eula=dict( + type='str', + disposition='/properties/*' + ), + privacy_statement_uri=dict( + type='str', + disposition='/properties/privacyStatementUri' + ), + release_note_uri=dict( + type='str', + disposition='/properties/releaseNoteUri' + ), + os_type=dict( + type='str', + disposition='/properties/osType', + choices=['windows', + 'linux'] + ), + os_state=dict( + type='str', + disposition='/properties/osState', + choices=['generalized', + 'specialized'] + ), + end_of_life_date=dict( + type='str', + disposition='/properties/endOfLifeDate' + ), + identifier=dict( + type='dict', + disposition='/properties/*', + options=dict( + publisher=dict( + type='str', + required=True, + updatable=False + ), + offer=dict( + type='str', + required=True + ), + sku=dict( + type='str', + required=True + ) + ) + ), + recommended=dict( + type='dict', + disposition='/properties/*', + options=dict( + v_cpus=dict( + type='dict', + disposition='vCPUs', + options=dict( + min=dict( + type='int' + ), + max=dict( + type='int' + ) + ) + ), + memory=dict( + type='dict', + options=dict( + min=dict( + type='int' + ), + max=dict( + type='int' + ) + ) + ) + ) + ), + disallowed=dict( + type='dict', + disposition='/properties/*', + options=dict( + disk_types=dict( + type='list', + disposition='diskTypes' + ) + ) + ), + purchase_plan=dict( + type='dict', + disposition='/properties/purchasePlan', + options=dict( + name=dict( + type='str' + ), + publisher=dict( + type='str' + ), + product=dict( + type='str' + ) + ) + ), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ) + ) + + self.resource_group = None + self.gallery_name = None + self.name = None + self.gallery_image = None + + self.results = dict(changed=False) + self.mgmt_client = None + self.state = None + self.url = None + self.status_code = [200, 201, 202] + self.to_do = Actions.NoAction + + self.body = {} + self.query_parameters = {} + self.query_parameters['api-version'] = '2019-03-01' + self.header_parameters = {} + self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' + + super(AzureRMGalleryImages, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=True) + + def exec_module(self, **kwargs): + for key in list(self.module_arg_spec.keys()): + if hasattr(self, key): + setattr(self, key, kwargs[key]) + elif kwargs[key] is not None: + self.body[key] = kwargs[key] + + self.inflate_parameters(self.module_arg_spec, self.body, 0) + + old_response = None + response = None + + self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, + base_url=self._cloud_environment.endpoints.resource_manager) + + resource_group = self.get_resource_group(self.resource_group) + + if 'location' not in self.body: + self.body['location'] = resource_group.location + + self.url = ('/subscriptions' + + '/{{ subscription_id }}' + + '/resourceGroups' + + '/{{ resource_group }}' + + '/providers' + + '/Microsoft.Compute' + + '/galleries' + + '/{{ gallery_name }}' + + '/images' + + '/{{ image_name }}') + self.url = self.url.replace('{{ subscription_id }}', self.subscription_id) + self.url = self.url.replace('{{ resource_group }}', self.resource_group) + self.url = self.url.replace('{{ gallery_name }}', self.gallery_name) + self.url = self.url.replace('{{ image_name }}', self.name) + + old_response = self.get_resource() + + if not old_response: + self.log("GalleryImage instance doesn't exist") + + if self.state == 'absent': + self.log("Old instance didn't exist") + else: + self.to_do = Actions.Create + else: + self.log('GalleryImage instance already exists') + + if self.state == 'absent': + self.to_do = Actions.Delete + else: + modifiers = {} + self.create_compare_modifiers(self.module_arg_spec, '', modifiers) + self.results['modifiers'] = modifiers + self.results['compare'] = [] + if not self.default_compare(modifiers, self.body, old_response, '', self.results): + self.to_do = Actions.Update + + if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): + self.log('Need to Create / Update the GalleryImage instance') + + if self.check_mode: + self.results['changed'] = True + return self.results + + response = self.create_update_resource() + + # if not old_response: + self.results['changed'] = True + # else: + # self.results['changed'] = old_response.__ne__(response) + self.log('Creation / Update done') + elif self.to_do == Actions.Delete: + self.log('GalleryImage instance deleted') + self.results['changed'] = True + + if self.check_mode: + return self.results + + self.delete_resource() + + # make sure instance is actually deleted, for some Azure resources, instance is hanging around + # for some time after deletion -- this should be really fixed in Azure + while self.get_resource(): + time.sleep(20) + else: + self.log('GalleryImage instance unchanged') + self.results['changed'] = False + response = old_response + + if response: + self.results["id"] = response["id"] + + return self.results + + def create_update_resource(self): + # self.log('Creating / Updating the GalleryImage instance {0}'.format(self.)) + + try: + response = self.mgmt_client.query(self.url, + 'PUT', + self.query_parameters, + self.header_parameters, + self.body, + self.status_code, + 600, + 30) + except CloudError as exc: + self.log('Error attempting to create the GalleryImage instance.') + self.fail('Error creating the GalleryImage instance: {0}'.format(str(exc))) + + try: + response = json.loads(response.text) + except Exception: + response = {'text': response.text} + pass + + return response + + def delete_resource(self): + # self.log('Deleting the GalleryImage instance {0}'.format(self.)) + try: + response = self.mgmt_client.query(self.url, + 'DELETE', + self.query_parameters, + self.header_parameters, + None, + self.status_code, + 600, + 30) + except CloudError as e: + self.log('Error attempting to delete the GalleryImage instance.') + self.fail('Error deleting the GalleryImage instance: {0}'.format(str(e))) + + return True + + def get_resource(self): + # self.log('Checking if the GalleryImage instance {0} is present'.format(self.)) + found = False + try: + response = self.mgmt_client.query(self.url, + 'GET', + self.query_parameters, + self.header_parameters, + None, + self.status_code, + 600, + 30) + response = json.loads(response.text) + found = True + self.log("Response : {0}".format(response)) + # self.log("AzureFirewall instance : {0} found".format(response.name)) + except CloudError as e: + self.log('Did not find the AzureFirewall instance.') + if found is True: + return response + + return False + + +def main(): + AzureRMGalleryImages() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_galleryimageversion.py b/lib/ansible/modules/cloud/azure/azure_rm_galleryimageversion.py new file mode 100644 index 00000000000..1af6d89b844 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_galleryimageversion.py @@ -0,0 +1,459 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 Zim Kalinowski, (@zikalino) +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: azure_rm_galleryimageversion +version_added: '2.9' +short_description: Manage Azure SIG Image Version instance. +description: + - 'Create, update and delete instance of Azure SIG Image Version.' +options: + resource_group: + description: + - The name of the resource group. + required: true + type: str + gallery_name: + description: + - >- + The name of the Shared Image Gallery in which the Image Definition + resides. + required: true + type: str + gallery_image_name: + description: + - >- + The name of the gallery Image Definition in which the Image Version is + to be created. + required: true + type: str + name: + description: + - >- + The name of the gallery Image Version to be created. Needs to follow + semantic version name pattern: The allowed characters are digit and + period. Digits must be within the range of a 32-bit integer. Format: + .. + required: true + type: str + location: + description: + - Resource location + type: str + publishing_profile: + description: + - Publishing profile. + required: true + type: dict + suboptions: + target_regions: + description: + - >- + The target regions where the Image Version is going to be replicated + to. This property is updatable. + type: list + suboptions: + name: + description: + - Region name. + type: str + regional_replica_count: + description: + - >- + The number of replicas of the Image Version to be created per + region. This property would take effect for a region when + regionalReplicaCount is not specified. This property is updatable. + type: str + storage_account_type: + description: + - Storage account type. + type: str + managed_image: + description: + - Managed image reference, could be resource id, or dictionary containing C(resource_group) and C(name) + required: true + type: raw + replica_count: + description: + - >- + The number of replicas of the Image Version to be created per + region. This property would take effect for a region when + regionalReplicaCount is not specified. This property is updatable. + type: number + exclude_from_latest: + description: + - >- + If set to true, Virtual Machines deployed from the latest version of + the Image Definition won't use this Image Version. + type: bool + end_of_life_date: + description: + - >- + The end of life date of the gallery Image Version. This property can + be used for decommissioning purposes. This property is updatable. + Format should be according to ISO-8601, for instance "2019-06-26". + type: str + storage_account_type: + description: + - >- + Specifies the storage account type to be used to store the image. + This property is not updatable. + type: str + state: + description: + - Assert the state of the GalleryImageVersion. + - >- + Use C(present) to create or update an GalleryImageVersion and C(absent) + to delete it. + default: present + choices: + - absent + - present + type: str +extends_documentation_fragment: + - azure + - azure_tags +author: + - Zim Kalinowski (@zikalino) + +''' + +EXAMPLES = ''' +- name: Create or update a simple gallery Image Version. + azure_rm_galleryimageversion: + resource_group: myResourceGroup + gallery_name: myGallery1283 + gallery_image_name: myImage + name: 10.1.3 + location: West US + publishing_profile: + end_of_life_date: "2020-10-01t00:00:00+00:00" + exclude_from_latest: yes + replica_count: 3 + storage_account_type: Standard_LRS + target_regions: + - name: West US + regional_replica_count: 1 + - name: East US + regional_replica_count: 2 + storage_account_type: Standard_ZRS + managed_image: + name: myImage + resource_group: myResourceGroup +''' + +RETURN = ''' +id: + description: + - Resource Id + returned: always + type: str + sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Compute/galleries/myGalle + ry1283/images/myImage/versions/10.1.3" +''' + +import time +import json +import re +from ansible.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt +from ansible.module_utils.azure_rm_common_rest import GenericRestClient +from copy import deepcopy +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class Actions: + NoAction, Create, Update, Delete = range(4) + + +class AzureRMGalleryImageVersions(AzureRMModuleBaseExt): + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + updatable=False, + disposition='resourceGroupName', + required=True + ), + gallery_name=dict( + type='str', + updatable=False, + disposition='galleryName', + required=True + ), + gallery_image_name=dict( + type='str', + updatable=False, + disposition='galleryImageName', + required=True + ), + name=dict( + type='str', + updatable=False, + disposition='galleryImageVersionName', + required=True + ), + location=dict( + type='str', + updatable=False, + disposition='/' + ), + publishing_profile=dict( + type='dict', + disposition='/properties/publishingProfile', + options=dict( + target_regions=dict( + type='list', + disposition='targetRegions', + options=dict( + name=dict( + type='str', + required=True + ), + regional_replica_count=dict( + type='int', + disposition='regionalReplicaCount' + ), + storage_account_type=dict( + type='str', + disposition='storageAccountType' + ) + ) + ), + managed_image=dict( + type='raw', + pattern=('/subscriptions/{subscription_id}/resourceGroups' + '/{resource_group}/providers/Microsoft.Compute' + '/images/{name}'), + disposition='source/managedImage/id' + ), + replica_count=dict( + type='int', + disposition='replicaCount' + ), + exclude_from_latest=dict( + type='bool', + disposition='excludeFromLatest' + ), + end_of_life_date=dict( + type='str', + disposition='endOfLifeDate' + ), + storage_account_type=dict( + type='str', + disposition='storageAccountType', + choices=['Standard_LRS', + 'Standard_ZRS'] + ) + ) + ), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ) + ) + + self.resource_group = None + self.gallery_name = None + self.gallery_image_name = None + self.name = None + self.gallery_image_version = None + + self.results = dict(changed=False) + self.mgmt_client = None + self.state = None + self.url = None + self.status_code = [200, 201, 202] + self.to_do = Actions.NoAction + + self.body = {} + self.query_parameters = {} + self.query_parameters['api-version'] = '2019-03-01' + self.header_parameters = {} + self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' + + super(AzureRMGalleryImageVersions, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=True) + + def exec_module(self, **kwargs): + for key in list(self.module_arg_spec.keys()): + if hasattr(self, key): + setattr(self, key, kwargs[key]) + elif kwargs[key] is not None: + self.body[key] = kwargs[key] + + self.inflate_parameters(self.module_arg_spec, self.body, 0) + + old_response = None + response = None + + self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, + base_url=self._cloud_environment.endpoints.resource_manager) + + resource_group = self.get_resource_group(self.resource_group) + + if 'location' not in self.body: + self.body['location'] = resource_group.location + + self.url = ('/subscriptions' + + '/{{ subscription_id }}' + + '/resourceGroups' + + '/{{ resource_group }}' + + '/providers' + + '/Microsoft.Compute' + + '/galleries' + + '/{{ gallery_name }}' + + '/images' + + '/{{ image_name }}' + + '/versions' + + '/{{ version_name }}') + self.url = self.url.replace('{{ subscription_id }}', self.subscription_id) + self.url = self.url.replace('{{ resource_group }}', self.resource_group) + self.url = self.url.replace('{{ gallery_name }}', self.gallery_name) + self.url = self.url.replace('{{ image_name }}', self.gallery_image_name) + self.url = self.url.replace('{{ version_name }}', self.name) + + old_response = self.get_resource() + + if not old_response: + self.log("GalleryImageVersion instance doesn't exist") + + if self.state == 'absent': + self.log("Old instance didn't exist") + else: + self.to_do = Actions.Create + else: + self.log('GalleryImageVersion instance already exists') + + if self.state == 'absent': + self.to_do = Actions.Delete + else: + modifiers = {} + self.create_compare_modifiers(self.module_arg_spec, '', modifiers) + self.results['modifiers'] = modifiers + self.results['compare'] = [] + if not self.default_compare(modifiers, self.body, old_response, '', self.results): + self.to_do = Actions.Update + + if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): + self.log('Need to Create / Update the GalleryImageVersion instance') + + if self.check_mode: + self.results['changed'] = True + return self.results + + response = self.create_update_resource() + + self.results['changed'] = True + self.log('Creation / Update done') + elif self.to_do == Actions.Delete: + self.log('GalleryImageVersion instance deleted') + self.results['changed'] = True + + if self.check_mode: + return self.results + + self.delete_resource() + else: + self.log('GalleryImageVersion instance unchanged') + self.results['changed'] = False + response = old_response + + if response: + self.results["id"] = response["id"] + self.results["old_response"] = response + + return self.results + + def create_update_resource(self): + # self.log('Creating / Updating the GalleryImageVersion instance {0}'.format(self.)) + + try: + response = self.mgmt_client.query(self.url, + 'PUT', + self.query_parameters, + self.header_parameters, + self.body, + self.status_code, + 600, + 30) + except CloudError as exc: + self.log('Error attempting to create the GalleryImageVersion instance.') + self.fail('Error creating the GalleryImageVersion instance: {0}'.format(str(exc))) + + try: + response = json.loads(response.text) + except Exception: + response = {'text': response.text} + pass + + while response['properties']['provisioningState'] == 'Creating': + time.sleep(60) + response = self.get_resource() + + return response + + def delete_resource(self): + # self.log('Deleting the GalleryImageVersion instance {0}'.format(self.)) + try: + response = self.mgmt_client.query(self.url, + 'DELETE', + self.query_parameters, + self.header_parameters, + None, + self.status_code, + 600, + 30) + except CloudError as e: + self.log('Error attempting to delete the GalleryImageVersion instance.') + self.fail('Error deleting the GalleryImageVersion instance: {0}'.format(str(e))) + return True + + def get_resource(self): + # self.log('Checking if the GalleryImageVersion instance {0} is present'.format(self.)) + found = False + try: + response = self.mgmt_client.query(self.url, + 'GET', + self.query_parameters, + self.header_parameters, + None, + self.status_code, + 600, + 30) + response = json.loads(response.text) + found = True + self.log("Response : {0}".format(response)) + # self.log("AzureFirewall instance : {0} found".format(response.name)) + except CloudError as e: + self.log('Did not find the AzureFirewall instance.') + if found is True: + return response + + return False + + +def main(): + AzureRMGalleryImageVersions() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/doc_fragments/azure.py b/lib/ansible/plugins/doc_fragments/azure.py index 2aea0a80471..c990a05641a 100644 --- a/lib/ansible/plugins/doc_fragments/azure.py +++ b/lib/ansible/plugins/doc_fragments/azure.py @@ -20,6 +20,7 @@ options: description: - Active Directory user password. Use when authenticating with an Active Directory user rather than service principal. + type: str profile: description: - Security profile found in ~/.azure/credentials file. diff --git a/test/integration/targets/azure_rm_gallery/aliases b/test/integration/targets/azure_rm_gallery/aliases new file mode 100644 index 00000000000..31f61844e79 --- /dev/null +++ b/test/integration/targets/azure_rm_gallery/aliases @@ -0,0 +1,5 @@ +cloud/azure +shippable/azure/group4 +destructive +azure_rm_galleryimage +azure_rm_galleryimageversion diff --git a/test/integration/targets/azure_rm_gallery/meta/main.yml b/test/integration/targets/azure_rm_gallery/meta/main.yml new file mode 100644 index 00000000000..95e1952f989 --- /dev/null +++ b/test/integration/targets/azure_rm_gallery/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/test/integration/targets/azure_rm_gallery/tasks/main.yml b/test/integration/targets/azure_rm_gallery/tasks/main.yml new file mode 100644 index 00000000000..b0d477778e1 --- /dev/null +++ b/test/integration/targets/azure_rm_gallery/tasks/main.yml @@ -0,0 +1,263 @@ +- name: Prepare random number + set_fact: + rpfx: "{{ resource_group | hash('md5') | truncate(7, True, '') }}{{ 1000 | random }}" + run_once: yes + +- name: Create virtual network + azure_rm_virtualnetwork: + resource_group: "{{ resource_group }}" + name: testVnet + address_prefixes: "10.0.0.0/16" + +- name: Add subnet + azure_rm_subnet: + resource_group: "{{ resource_group }}" + name: testSubnet + address_prefix: "10.0.1.0/24" + virtual_network: testVnet + +- name: Create public IP address + azure_rm_publicipaddress: + resource_group: "{{ resource_group }}" + allocation_method: Static + name: testPublicIP + +- name: Create virtual network inteface cards for VM A and B + azure_rm_networkinterface: + resource_group: "{{ resource_group }}" + name: "vmforimage{{ rpfx }}nic" + virtual_network: testVnet + subnet: testSubnet + +- name: Create VM + azure_rm_virtualmachine: + resource_group: "{{ resource_group }}" + name: "vmforimage{{ rpfx }}" + admin_username: testuser + admin_password: "Password1234!" + vm_size: Standard_B1ms + network_interfaces: "vmforimage{{ rpfx }}nic" + image: + offer: UbuntuServer + publisher: Canonical + sku: 16.04-LTS + version: latest +- name: Generalize VM + azure_rm_virtualmachine: + resource_group: "{{ resource_group }}" + name: "vmforimage{{ rpfx }}" + generalized: yes +- name: Create custom image + azure_rm_image: + resource_group: "{{ resource_group }}" + name: testimagea + source: "vmforimage{{ rpfx }}" +- name: Create or update a simple gallery. + azure_rm_gallery: + resource_group: "{{ resource_group }}" + name: myGallery{{ rpfx }} + location: West US + description: This is the gallery description. + register: output + +- assert: + that: + - output.changed + +- name: Create or update a simple gallery - idempotent + azure_rm_gallery: + resource_group: "{{ resource_group }}" + name: myGallery{{ rpfx }} + location: West US + description: This is the gallery description. + register: output + +- assert: + that: + - not output.changed + +- name: Create or update a simple gallery - change description + azure_rm_gallery: + resource_group: "{{ resource_group }}" + name: myGallery{{ rpfx }} + location: West US + description: This is the gallery description - xxx. + register: output + +- assert: + that: + - output.changed + +- name: Create or update gallery image + azure_rm_galleryimage: + resource_group: "{{ resource_group }}" + gallery_name: myGallery{{ rpfx }} + name: myImage + location: West US + os_type: linux + os_state: generalized + identifier: + publisher: myPublisherName + offer: myOfferName + sku: mySkuName + description: Image Description + register: output + +- assert: + that: + - output.changed + +- name: Create or update gallery image - idempotent + azure_rm_galleryimage: + resource_group: "{{ resource_group }}" + gallery_name: myGallery{{ rpfx }} + name: myImage + location: West US + os_type: linux + os_state: generalized + identifier: + publisher: myPublisherName + offer: myOfferName + sku: mySkuName + description: Image Description + register: output + +- assert: + that: + - not output.changed + +- name: Create or update gallery image - change description + azure_rm_galleryimage: + resource_group: "{{ resource_group }}" + gallery_name: myGallery{{ rpfx }} + name: myImage + location: West US + os_type: linux + os_state: generalized + identifier: + publisher: myPublisherName + offer: myOfferName + sku: mySkuName + description: Image Description XXXs + register: output + +- assert: + that: + - output.changed + +- name: Create or update a simple gallery Image Version. + azure_rm_galleryimageversion: + resource_group: "{{ resource_group }}" + gallery_name: myGallery{{ rpfx }} + gallery_image_name: myImage + name: 10.1.3 + location: West US + publishing_profile: + end_of_life_date: "2020-10-01t00:00:00+00:00" + exclude_from_latest: yes + replica_count: 3 + storage_account_type: Standard_LRS + target_regions: + - name: West US + regional_replica_count: 1 + - name: East US + regional_replica_count: 2 + storage_account_type: Standard_ZRS + managed_image: + name: testimagea + resource_group: "{{ resource_group }}" + register: output + +- assert: + that: + - output.changed + +- name: Create or update a simple gallery Image Version - idempotent + azure_rm_galleryimageversion: + resource_group: "{{ resource_group }}" + gallery_name: myGallery{{ rpfx }} + gallery_image_name: myImage + name: 10.1.3 + location: West US + publishing_profile: + end_of_life_date: "2020-10-01t00:00:00+00:00" + exclude_from_latest: yes + replica_count: 3 + storage_account_type: Standard_LRS + target_regions: + - name: West US + regional_replica_count: 1 + - name: East US + regional_replica_count: 2 + storage_account_type: Standard_ZRS + managed_image: + name: testimagea + resource_group: "{{ resource_group }}" + register: output + +- assert: + that: + - not output.changed + +- name: Create or update a simple gallery Image Version - change end of life + azure_rm_galleryimageversion: + resource_group: "{{ resource_group }}" + gallery_name: myGallery{{ rpfx }} + gallery_image_name: myImage + name: 10.1.3 + location: West US + publishing_profile: + end_of_life_date: "2021-10-01t00:00:00+00:00" + exclude_from_latest: yes + replica_count: 3 + storage_account_type: Standard_LRS + target_regions: + - name: West US + regional_replica_count: 1 + - name: East US + regional_replica_count: 2 + storage_account_type: Standard_ZRS + managed_image: + name: testimagea + resource_group: "{{ resource_group }}" + register: output + +- assert: + that: + - output.changed + +- name: Delete gallery image Version. + azure_rm_galleryimageversion: + resource_group: "{{ resource_group }}" + gallery_name: myGallery{{ rpfx }} + gallery_image_name: myImage + name: 10.1.3 + state: absent + register: output + +- assert: + that: + - output.changed + +- name: Delete gallery image + azure_rm_galleryimage: + resource_group: "{{ resource_group }}" + gallery_name: myGallery{{ rpfx }} + name: myImage + state: absent + register: output + +- assert: + that: + - output.changed + +- name: Delete gallery + azure_rm_gallery: + resource_group: "{{ resource_group }}" + name: myGallery{{ rpfx }} + state: absent + register: output + +- assert: + that: + - output.changed diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt index c70d4ca0599..25ad0459f81 100644 --- a/test/sanity/validate-modules/ignore.txt +++ b/test/sanity/validate-modules/ignore.txt @@ -442,7 +442,6 @@ lib/ansible/modules/cloud/azure/azure_rm_loadbalancer.py E337 lib/ansible/modules/cloud/azure/azure_rm_loganalyticsworkspace_facts.py E337 lib/ansible/modules/cloud/azure/azure_rm_loganalyticsworkspace.py E337 lib/ansible/modules/cloud/azure/azure_rm_manageddisk_facts.py E325 -lib/ansible/modules/cloud/azure/azure_rm_manageddisk_facts.py E337 lib/ansible/modules/cloud/azure/azure_rm_manageddisk.py E337 lib/ansible/modules/cloud/azure/azure_rm_mariadbconfiguration_facts.py E337 lib/ansible/modules/cloud/azure/azure_rm_mariadbconfiguration.py E337