diff --git a/lib/ansible/modules/cloud/azure/azure_rm_managed_disk.py b/lib/ansible/modules/cloud/azure/azure_rm_managed_disk.py new file mode 100644 index 0000000000..a22a7f2c47 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_managed_disk.py @@ -0,0 +1,339 @@ +#!/usr/bin/python +# +# Copyright (c) 2017 Bruno Medina Bolanos Cacho +# +# 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_managed_disk + +version_added: "2.4" + +short_description: Manage Azure Manage Disks + +description: + - Create, update and delete an Azure Managed Disk + +options: + resource_group: + description: + - "Name of a resource group where the managed disk exists or will be created." + required: true + name: + description: + - Name of the managed disk + required: true + state: + description: + - Assert the state of the managed disk. Use 'present' to create or update a managed disk and + 'absent' to delete a managed disk. + default: present + choices: + - absent + - present + required: false + location: + description: + - Valid Azure location. Defaults to location of the resource group. + default: resource_group location + required: false + storage_account_type: + description: + - "Type of storage for the managed disk: 'Standard_LRS' or 'Premium_LRS'. If not specified the disk is created 'Standard_LRS'" + choices: + - Standard_LRS + - Premium_LRS + required: false + create_option: + description: + - "Allowed values: empty, import, copy. 'import' from a VHD file in 'source_uri' and 'copy' from previous managed disk 'source_resource_uri'." + choices: + - empty + - import + - copy + required: false + source_uri: + description: + - URI to a valid VHD file to be used when 'create_option' is 'import'. + required: false + source_resource_uri: + description: + - The resource ID of the managed disk to copy when 'create_option' is 'copy'. + required: false + os_type: + description: + - "Type of Operating System: 'linux' or 'windows'. Used when 'create_option' is either 'copy' or 'import' and the source is an OS disk." + choices: + - linux + - windows + required: false + disk_size_gb: + description: + -Size in GB of the managed disk to be created. If 'create_option' is 'copy' then the value must be greater than or equal to the source's size. + required: true + tags: + description: + - Tags to assign to the managed disk. + required: false + +extends_documentation_fragment: + - azure + - azure_tags +author: + - "Bruno Medina (@brusMX)" +''' + +EXAMPLES = ''' + - name: Create managed disk + azure_rm_managed_disk: + name: mymanageddisk + location: eastus + resource_group: Testing + disk_size_gb: 4 + + - name: Delete managed disk + azure_rm_manage_disk: + name: mymanageddisk + location: eastus + resource_group: Testing + state: absent +''' + +RETURN = ''' +id: + description: The managed disk resource ID. + returned: always + type: dict +state: + description: Current state of the managed disk + returned: always + type: dict +changed: + description: Whether or not the resource has changed + returned: always + type: bool +''' + +import re + + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase +try: + from msrestazure.azure_exceptions import CloudError + from azure.mgmt.compute.models import DiskCreateOption + from azure.mgmt.compute.models import DiskSku +except ImportError: + # This is handled in azure_rm_common + pass + + +def managed_disk_to_dict(managed_disk): + os_type = None + if managed_disk.os_type: + os_type = managed_disk.os_type.name + return dict( + id=managed_disk.id, + name=managed_disk.name, + location=managed_disk.location, + tags=managed_disk.tags, + disk_size_gb=managed_disk.disk_size_gb, + os_type=os_type, + storage_account_type='Premium_LRS' if managed_disk.sku.tier == 'Premium' else 'Standard_LRS' + ) + + +class AzureRMManagedDisk(AzureRMModuleBase): + """Configuration class for an Azure RM Managed Disk resource""" + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + required=True + ), + name=dict( + type='str', + required=True + ), + state=dict( + type='str', + required=False, + default='present', + choices=['present', 'absent'] + ), + location=dict( + type='str', + required=False + ), + storage_account_type=dict( + type='str', + required=False, + choices=['Standard_LRS', 'Premium_LRS'] + ), + create_option=dict( + type='str', + required=False, + choices=['empty', 'import', 'copy'] + ), + source_uri=dict( + type='str', + required=False + ), + source_resource_uri=dict( + type='str', + required=False + ), + os_type=dict( + type='str', + required=False, + choices=['linux', 'windows'] + ), + disk_size_gb=dict( + type='int', + required=False + ) + ) + required_if = [ + ('create_option', 'import', ['source_uri']), + ('create_option', 'copy', ['source_resource_uri']), + ('state', 'present', ['disk_size_gb']) + ] + self.results = dict( + changed=False, + state=dict()) + + self.resource_group = None + self.name = None + self.location = None + self.storage_account_type = None + self.create_option = None + self.source_uri = None + self.source_resource_uri = None + self.os_type = None + self.disk_size_gb = None + self.tags = None + super(AzureRMManagedDisk, self).__init__( + derived_arg_spec=self.module_arg_spec, + required_if=required_if, + supports_check_mode=True, + supports_tags=True) + + def exec_module(self, **kwargs): + """Main module execution method""" + for key in list(self.module_arg_spec.keys()) + ['tags']: + setattr(self, key, kwargs[key]) + results = dict() + resource_group = None + response = None + try: + resource_group = self.get_resource_group(self.resource_group) + except CloudError: + self.fail( + 'resource group {} not found' + .format(self.resource_group)) + if not self.location: + self.location = resource_group.location + if self.state == 'present': + self.results['state'] = self.create_or_update_managed_disk() + elif self.state == 'absent': + self.delete_managed_disk() + return self.results + + def create_or_update_managed_disk(self): + # Scaffolding empty managed disk + disk_params = {} + creation_data = {} + disk_params['location'] = self.location + disk_params['tags'] = self.tags + if self.storage_account_type: + storage_account_type = DiskSku(self.storage_account_type) + disk_params['sku'] = storage_account_type + disk_params['disk_size_gb'] = self.disk_size_gb + # TODO: Add support for EncryptionSettings + creation_data['create_option'] = DiskCreateOption.empty + if self.create_option == 'import': + creation_data['create_option'] = DiskCreateOption.import_enum + creation_data['source_uri'] = self.source_uri + elif self.create_option == 'copy': + creation_data['create_option'] = DiskCreateOption.copy + creation_data['source_resource_id'] = self.source_resource_uri + try: + # CreationData cannot be changed after creation + disk_params['creation_data'] = creation_data + found_prev_disk = self.get_managed_disk() + if found_prev_disk: + if not self.is_different(found_prev_disk, disk_params): + return found_prev_disk + if not self.check_mode: + poller = self.compute_client.disks.create_or_update( + self.resource_group, + self.name, + disk_params) + aux = self.get_poller_result(poller) + result = managed_disk_to_dict(aux) + else: + result = True + self.results['changed'] = True + except CloudError as e: + self.fail("Error creating the managed disk: {0}".format(str(e))) + return result + + # This method accounts for the difference in structure between the + # Azure retrieved disk and the parameters for the new disk to be created. + def is_different(self, found_disk, new_disk): + resp = False + if new_disk.get('disk_size_gb'): + if not found_disk['disk_size_gb'] == new_disk['disk_size_gb']: + resp = True + if new_disk.get('sku'): + if not found_disk['storage_account_type'] == new_disk['sku'].name: + resp = True + # Check how to implement tags + if new_disk.get('tags') is not None: + if not found_disk['tags'] == new_disk['tags']: + resp = True + return resp + + def delete_managed_disk(self): + try: + if not self.check_mode: + poller = self.compute_client.disks.delete( + self.resource_group, + self.name) + result = self.get_poller_result(poller) + else: + result = True + self.results['changed'] = True + except CloudError as e: + self.fail("Error deleting the managed disk: {0}".format(str(e))) + return result + + def get_managed_disk(self): + resp = False + try: + resp = self.compute_client.disks.get( + self.resource_group, + self.name) + except CloudError as e: + self.log('Did not find managed disk') + if resp: + resp = managed_disk_to_dict( + resp) + return resp + + +def main(): + """Main execution""" + AzureRMManagedDisk() + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_managed_disk_facts.py b/lib/ansible/modules/cloud/azure/azure_rm_managed_disk_facts.py new file mode 100644 index 0000000000..dc89e5af05 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_managed_disk_facts.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# +# Copyright (c) 2016 Bruno Medina Bolanos Cacho, +# +# 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. +# +# 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_managed_disk_facts + +version_added: "2.4" + +short_description: Get managed disk facts. + +description: + - Get facts for a specific managed disk or all managed disks. + +options: + name: + description: + - Limit results to a specific managed disk + required: false + default: null + resource_group: + description: + - Limit results to a specific resource group + required: false + default: null + tags: + description: + - Limit results by providing a list of tags. Format tags as 'key' or 'key:value'. + required: false + default: null + +extends_documentation_fragment: + - azure + +author: + - "Bruno Medina (@brusMX)" +''' + +EXAMPLES = ''' + - name: Get facts for one managed disk + azure_rm_managed_disk_facts: + name: Testing + resource_group: TestRG + + - name: Get facts for all managed disks + azure_rm_managed_disk_facts: + + - name: Get facts by tags + azure_rm_managed_disk_facts: + tags: + - testing +''' + +RETURN = ''' +azure_managed_disk: + description: List of managed disk dicts. + returned: always + type: list +''' + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase + +try: + from msrestazure.azure_exceptions import CloudError +except: + # handled in azure_rm_common + pass + + +def managed_disk_to_dict(managed_disk): + os_type = None + if managed_disk.os_type: + os_type = managed_disk.os_type.name + return dict( + id=managed_disk.id, + name=managed_disk.name, + location=managed_disk.location, + tags=managed_disk.tags, + disk_size_gb=managed_disk.disk_size_gb, + os_type=os_type, + storage_account_type='Premium_LRS' if managed_disk.sku.tier == 'Premium' else 'Standard_LRS' + ) + + +class AzureRMManagedDiskFacts(AzureRMModuleBase): + """Utility class to get managed disk facts""" + + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + required=False + ), + name=dict( + type='str', + required=False + ), + state=dict( + type='str', + required=False, + default='present', + choices=['present', 'absent'] + ), + location=dict( + type='str', + required=False + ), + storage_account_type=dict( + type='str', + required=False, + choices=['Standard_LRS', 'Premium_LRS'] + ), + os_type=dict( + type='str', + required=False, + choices=['linux', 'windows'] + ), + disk_size_gb=dict( + type='int', + required=False + ), + tags=dict( + type='str', + required=False + ), + ) + self.results = dict( + ansible_facts=dict( + azure_managed_disk=[] + ) + ) + self.resource_group = None + self.name = None + self.location = None + self.storage_account_type = None + self.create_option = None + self.source_uri = None + self.source_resource_uri = None + self.os_type = None + self.disk_size_gb = None + self.tags = None + super(AzureRMManagedDiskFacts, self).__init__( + derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=True) + + def exec_module(self, **kwargs): + for key in self.module_arg_spec: + setattr(self, key, kwargs[key]) + + self.results['ansible_facts']['azure_managed_disk'] = ( + self.get_item() if self.name + else self.list_items() + ) + + return self.results + + def get_item(self): + """Get a single managed disk""" + item = None + result = [] + + try: + item = self.compute_client.disks.get( + self.resource_group, + self.name) + except CloudError: + pass + + if item and self.has_tags(item.tags, self.tags): + result = [managed_disk_to_dict(item)] + + return result + + def list_items(self): + """Get all managed disks""" + try: + response = self.compute_client.disks.list() + except CloudError as exc: + self.fail('Failed to list all items - {}'.format(str(exc))) + + results = [] + for item in response: + if self.has_tags(item.tags, self.tags): + results.append(managed_disk_to_dict(item)) + return results + + +def main(): + """Main module execution code path""" + + AzureRMManagedDiskFacts() + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/azure_rm_managed_disk/aliases b/test/integration/targets/azure_rm_managed_disk/aliases new file mode 100644 index 0000000000..d6ff84111c --- /dev/null +++ b/test/integration/targets/azure_rm_managed_disk/aliases @@ -0,0 +1,3 @@ +cloud/azure +posix/ci/cloud/azure +destructive diff --git a/test/integration/targets/azure_rm_managed_disk/meta/main.yml b/test/integration/targets/azure_rm_managed_disk/meta/main.yml new file mode 100644 index 0000000000..95e1952f98 --- /dev/null +++ b/test/integration/targets/azure_rm_managed_disk/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/test/integration/targets/azure_rm_managed_disk/tasks/main.yml b/test/integration/targets/azure_rm_managed_disk/tasks/main.yml new file mode 100644 index 0000000000..f13ae65f52 --- /dev/null +++ b/test/integration/targets/azure_rm_managed_disk/tasks/main.yml @@ -0,0 +1,221 @@ + - name: Setup variables... + set_fact: + managed_disk1: "{{ resource_group | hash('md5') | truncate(24, True, '') }}" + managed_disk2: "{{ resource_group | hash('md5') | truncate(18, True, '') }}" + + - name: Clearing (if) previous disks were created (1/2) + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk2 }}" + state: absent + + - name: Clearing (if) previous disks were created (2/2) + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + state: absent + + - name: Create managed disk (Check Mode) + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 1 + tags: + testing: testing + delete: never + register: output + check_mode: yes + + - name: Assert status succeeded (Check Mode) + assert: + that: + - output.changed + - output.state + + - name: Test invalid account name (should give error) + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "invalid_char$" + disk_size_gb: 1 + state: present + register: output + ignore_errors: yes + check_mode: no + + - name: Assert task failed + assert: { that: "output['failed'] == True" } + + - name: Create new managed disk succesfully + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + storage_account_type: "Standard_LRS" + disk_size_gb: 1 + tags: + testing: testing + delete: never + register: output + + - name: Assert status succeeded and results include an Id value + assert: + that: + - output.changed + - output.state.id is defined + + - name: Copy disk to a new managed disk + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk2 }}" + create_option: "copy" + source_resource_uri: "{{ output.state.id }}" + disk_size_gb: 1 + register: copy + + - name: Assert status succeeded and results include an Id value + assert: + that: + - copy.changed + - copy.state.id is defined + + - name: Update a new disk without changes + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + storage_account_type: "Standard_LRS" + disk_size_gb: 1 + register: output + + - name: Assert status succeeded and results include an Id value + assert: + that: + - not output.changed + - output.state.id is defined + + - name: Change storage account type to an invalid type + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + storage_account_type: "PremiumL" + disk_size_gb: 1 + register: output + ignore_errors: yes + + - name: Assert storage account type change failed + assert: { that: "output['failed'] == True" } + + - name: Change disk size to incompatible size + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 30000 + register: output + ignore_errors: yes + + - name: Assert disk size change to incompatible size (>4095) failure + assert: { that: "output['failed'] == True" } + + - name: Change disk to bigger size + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 2 + register: output + + - name: Assert status succeeded + assert: + that: + - output.changed + + - name: Change disk to Premium + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + storage_account_type: "Premium_LRS" + disk_size_gb: 2 + register: output + + - name: Assert status succeeded + assert: + that: + - output.changed + + - name: Update disk tags + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 2 + tags: + testing: testing + delete: never + galaxy: 'no' + register: output + + - name: Assert disk incremented tags + assert: + that: + - "output.state.tags | length == 3" + - "output.state.tags.galaxy == 'no'" + + - name: Update disk tags + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 2 + tags: + testing: testing + delete: never + register: output + + - name: Assert disk tags are 2 + assert: + that: + - "output.state.tags | length == 2" + - "output.state.tags.testing == 'testing'" + - "output.state.tags.delete == 'never'" + + - name: Gather facts to one specific disk + azure_rm_managed_disk_facts: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + + - assert: + that: + - "azure_managed_disk | length == 1" + + - name: Gather facts + azure_rm_managed_disk_facts: + resource_group: "{{ resource_group }}" + + - assert: + that: + - "azure_managed_disk | length > 0" + + - name: Delete managed disk (Check Mode) + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + state: absent + disk_size_gb: 2 + check_mode: yes + + - name: Assert status succeeded + assert: + that: + - output.changed + - output.state + + - name: Delete managed disk + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk2 }}" + disk_size_gb: 1 + state: absent + check_mode: no + + - name: Delete copied managed disk + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 2 + state: absent + check_mode: no \ No newline at end of file