From ecd995496c74d043728652f70a04d1b19c6f0e78 Mon Sep 17 00:00:00 2001 From: Yunge Zhu <37337818+yungezz@users.noreply.github.com> Date: Thu, 29 Nov 2018 07:59:55 +0800 Subject: [PATCH] new module: azure_rm_rediscache (#47914) * add new redis cache module * fix version * fix lint --- .../cloud/azure/azure_rm_rediscache.py | 619 ++++++++++++++++++ packaging/requirements/requirements-azure.txt | 3 +- .../targets/azure_rm_rediscache/aliases | 3 + .../targets/azure_rm_rediscache/meta/main.yml | 2 + .../azure_rm_rediscache/tasks/main.yml | 167 +++++ .../requirements/integration.cloud.azure.txt | 3 +- 6 files changed, 795 insertions(+), 2 deletions(-) create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_rediscache.py create mode 100644 test/integration/targets/azure_rm_rediscache/aliases create mode 100644 test/integration/targets/azure_rm_rediscache/meta/main.yml create mode 100644 test/integration/targets/azure_rm_rediscache/tasks/main.yml diff --git a/lib/ansible/modules/cloud/azure/azure_rm_rediscache.py b/lib/ansible/modules/cloud/azure/azure_rm_rediscache.py new file mode 100644 index 0000000000..82dd5833c2 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_rediscache.py @@ -0,0 +1,619 @@ +#!/usr/bin/python +# +# Copyright (c) 2018 Yunge Zhu, +# +# 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_rediscache +version_added: "2.8" +short_description: Manage Azure Redis Cache instance. +description: + - Create, update and delete instance of Azure Redis Cache. + +options: + resource_group: + description: + - Name of the resource group to which the resource belongs. + required: True + name: + description: + - Unique name of the redis cache to create or update. To create or update a deployment slot, use the {slot} parameter. + required: True + + location: + description: + - Resource location. If not set, location from the resource group will be used as default. + sku: + description: Sku info of redis cache. + suboptions: + name: + description: Type of redis cache to deploy + choices: + - basic + - standard + - premium + required: True + size: + description: + - Size of redis cache to deploy. + - When I(sku) is C(basic) or C(standard), allowed values are C0, C1, C2, C3, C4, C5, C6. + - When I(sku) is C(premium), allowed values are P1, P2, P3, P4. + - Please see U(https://docs.microsoft.com/en-us/rest/api/redis/redis/create#sku) for allowed values. + choices: + - C0 + - C1 + - C2 + - C3 + - C4 + - C5 + - C6 + - P1 + - P2 + - P3 + - P4 + required: True + enable_non_ssl_port: + description: + - When true, the non-ssl redis server port 6379 will be enabled. + type: bool + default: false + maxfragmentationmemory_reserved: + description: + - Configures the amount of memory in MB that is reserved to accommodate for memory fragmentation. + - Please see U(https://docs.microsoft.com/en-us/azure/redis-cache/cache-configure#advanced-settings) for more detail. + maxmemory_reserved: + description: + - Configures the amount of memory in MB that is reserved for non-cache operations. + - Please see U(https://docs.microsoft.com/en-us/azure/redis-cache/cache-configure#advanced-settings) for more detail. + maxmemory_policy: + description: + - Configures the eviction policy of the cache. + - Please see U(https://docs.microsoft.com/en-us/azure/redis-cache/cache-configure#advanced-settings) for more detail. + choices: + - volatile_lru + - allkeys_lru + - volatile_random + - allkeys_random + - volatile_ttl + - noeviction + notify_keyspace_events: + description: + - Allows clients to receive notifications when certain events occur. + - Please see U(https://docs.microsoft.com/en-us/azure/redis-cache/cache-configure#advanced-settings) for more detail. + shard_count: + description: + - The number of shards to be created when I(sku) is C(premium). + type: int + static_ip: + description: + - Static IP address. Required when deploying a redis cache inside an existing Azure virtual network. + subnet: + description: + - Subnet in a virtual network to deploy the redis cache in. + - "It can be resource id of subnet, eg. + /subscriptions/{subid}/resourceGroups/{resourceGroupName}/Microsoft.{Network|ClassicNetwork}/VirtualNetworks/vnet1/subnets/subnet1" + - It can be a dictionary where contains C(name), C(virtual_network_name) and C(resource_group). + - C(name). Name of the subnet. + - C(resource_group). Resource group name of the subnet. + - C(virtual_network_name). Name of virtual network to which this subnet belongs. + tenant_settings: + description: + - Dict of tenant settings. + state: + description: + - Assert the state of the redis cahce. + - Use 'present' to create or update a redis cache and 'absent' to delete it. + default: present + choices: + - absent + - present + +extends_documentation_fragment: + - azure + - azure_tags + +author: + - "Yunge Zhu(@yungezz)" + +''' + +EXAMPLES = ''' + - name: Create a redis cache + azure_rm_rediscache: + resource_group: myResourceGroup + name: myRedisCache + sku: + name: basic + size: C1 + + + - name: Scale up the redis cache + azure_rm_rediscache: + resource_group: myResourceGroup + name: myRedisCache + sku: + name: standard + size: C1 + tags: + testing: foo + + - name: Create redis with subnet + azure_rm_rediscache: + resource_group: myResourceGroup + name: myRedisCache2 + sku: + name: premium + size: P1 + subnet: /subscriptions//resourceGroups/redistest1/providers/Microsoft.Network/virtualNetworks/testredisvnet1/subnets/subnet1 + +''' + +RETURN = ''' +id: + description: Id of the redis cache. + returned: always + type: string + sample: { + "id": "/subscriptions//resourceGroups/rg/providers/Microsoft.Cache/Redis/redis1" + } +host_name: + description: Host name of the redis cache. + returned: state is present + type: string + sample: { + "host_name": "redis1.redis.cache.windows.net" + } +''' + +import time +from ansible.module_utils.azure_rm_common import AzureRMModuleBase + +try: + from msrestazure.azure_exceptions import CloudError + from msrestazure.azure_operation import AzureOperationPoller + from msrest.serialization import Model + from azure.mgmt.redis import RedisManagementClient + from azure.mgmt.redis.models import (RedisCreateParameters, RedisUpdateParameters, Sku) +except ImportError: + # This is handled in azure_rm_common + pass + + +sku_spec = dict( + name=dict( + type='str', + choices=['basic', 'standard', 'premium']), + size=dict( + type='str', + choices=['C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'P1', 'P2', 'P3', 'P4'] + ) +) + + +def rediscache_to_dict(redis): + result = dict( + id=redis.id, + name=redis.name, + location=redis.location, + sku=dict( + name=redis.sku.name.lower(), + size=redis.sku.family + str(redis.sku.capacity) + ), + enable_non_ssl_port=redis.enable_non_ssl_port, + host_name=redis.host_name, + shard_count=redis.shard_count, + subnet=redis.subnet_id, + static_ip=redis.static_ip, + provisioning_state=redis.provisioning_state, + tenant_settings=redis.tenant_settings, + tags=redis.tags if redis.tags else None + ) + for key in redis.redis_configuration: + result[hyphen_to_underline(key)] = hyphen_to_underline(redis.redis_configuration.get(key, None)) + return result + + +def hyphen_to_underline(input): + if input and isinstance(input, str): + return input.replace("-", "_") + return input + + +def underline_to_hyphen(input): + if input and isinstance(input, str): + return input.replace("_", "-") + return input + + +class Actions: + NoAction, Create, Update, Delete = range(4) + + +class AzureRMRedisCaches(AzureRMModuleBase): + """Configuration class for an Azure RM Redis Cache resource""" + + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + required=True + ), + name=dict( + type='str', + required=True + ), + location=dict( + type='str' + ), + sku=dict( + type='dict', + options=sku_spec + ), + enable_non_ssl_port=dict( + type='bool', + default=False + ), + maxfragmentationmemory_reserved=dict( + type='int' + ), + maxmemory_reserved=dict( + type='int' + ), + maxmemory_policy=dict( + type='str', + choices=[ + "volatile_lru", + "allkeys_lru", + "volatile_random", + "allkeys_random", + "volatile_ttl", + "noeviction" + ] + ), + notify_keyspace_events=dict( + type='int' + ), + shard_count=dict( + type='ints' + ), + static_ip=dict( + type='str' + ), + subnet=dict( + type='raw' + ), + tenant_settings=dict( + type='dict' + ), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ) + ) + + self._client = None + + self.resource_group = None + self.name = None + self.location = None + + self.sku = None + self.size = None + self.enable_non_ssl_port = False + self.configuration_file_path = None + self.shard_count = None + self.static_ip = None + self.subnet = None + self.tenant_settings = None + + self.tags = None + + self.results = dict( + changed=False, + id=None, + host_name=None + ) + self.state = None + self.to_do = Actions.NoAction + + self.frameworks = None + + super(AzureRMRedisCaches, self).__init__(derived_arg_spec=self.module_arg_spec, + 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]) + + old_response = None + response = None + to_be_updated = False + + # define redis_configuration properties + self.redis_configuration_properties = ["maxfragmentationmemory_reserved", + "maxmemory_reserved", + "maxmemory_policy", + "notify_keyspace_events"] + + # get management client + self._client = self.get_mgmt_svc_client(RedisManagementClient, + base_url=self._cloud_environment.endpoints.resource_manager, + api_version='2018-03-01') + + # set location + resource_group = self.get_resource_group(self.resource_group) + if not self.location: + self.location = resource_group.location + + # check subnet exists + if self.subnet: + self.subnet = self.parse_subnet() + + # get existing redis cache + old_response = self.get_rediscache() + + if old_response: + self.results['id'] = old_response['id'] + + if self.state == 'present': + # if redis not exists + if not old_response: + self.log("Redis cache instance doesn't exist") + + to_be_updated = True + self.to_do = Actions.Create + + if not self.sku: + self.fail("Please specify sku to creating new redis cache.") + + else: + # redis exists already, do update + self.log("Redis cache instance already exists") + + update_tags, self.tags = self.update_tags(old_response.get('tags', None)) + + if update_tags: + to_be_updated = True + self.to_do = Actions.Update + + # check if update + if self.check_update(old_response): + to_be_updated = True + self.to_do = Actions.Update + + elif self.state == 'absent': + if old_response: + self.log("Delete Redis cache instance") + self.results['id'] = old_response['id'] + to_be_updated = True + self.to_do = Actions.Delete + else: + self.results['changed'] = False + self.log("Redis cache {0} not exists.".format(self.name)) + + if to_be_updated: + self.log('Need to Create/Update redis cache') + self.results['changed'] = True + + if self.check_mode: + return self.results + + if self.to_do == Actions.Create: + response = self.create_rediscache() + self.results['id'] = response['id'] + self.results['host_name'] = response['host_name'] + + if self.to_do == Actions.Update: + response = self.update_rediscache() + self.results['id'] = response['id'] + self.results['host_name'] = response['host_name'] + + if self.to_do == Actions.Delete: + self.delete_rediscache() + self.log('Redis cache instance deleted') + + return self.results + + def check_update(self, existing): + if self.enable_non_ssl_port is not None and existing['enable_non_ssl_port'] != self.enable_non_ssl_port: + self.log("enable_non_ssl_port diff: origin {0} / update {1}".format(existing['enable_non_ssl_port'], self.enable_non_ssl_port)) + return True + if self.sku is not None: + if existing['sku']['name'] != self.sku['name']: + self.log("sku diff: origin {0} / update {1}".format(existing['sku']['name'], self.sku['name'])) + return True + if existing['sku']['size'] != self.sku['size']: + self.log("size diff: origin {0} / update {1}".format(existing['sku']['size'], self.sku['size'])) + return True + if self.tenant_settings is not None and existing['tenant_settings'] != self.tenant_settings: + self.log("tenant_settings diff: origin {0} / update {1}".format(existing['tenant_settings'], self.tenant_settings)) + return True + if self.shard_count is not None and existing['shard_count'] != self.shard_count: + self.log("shard_count diff: origin {0} / update {1}".format(existing['shard_count'], self.shard_count)) + return True + if self.subnet is not None and existing['subnet'] != self.subnet: + self.log("subnet diff: origin {0} / update {1}".format(existing['subnet'], self.subnet)) + return True + if self.static_ip is not None and existing['static_ip'] != self.static_ip: + self.log("static_ip diff: origin {0} / update {1}".format(existing['static_ip'], self.static_ip)) + return True + for config in self.redis_configuration_properties: + if getattr(self, config) is not None and existing.get(config, None) != getattr(self, config, None): + self.log("redis_configuration {0} diff: origin {1} / update {2}".format(config, existing.get(config, None), getattr(self, config, None))) + return True + return False + + def create_rediscache(self): + ''' + Creates redis cache instance with the specified configuration. + + :return: deserialized redis cache instance state dictionary + ''' + self.log( + "Creating redis cache instance {0}".format(self.name)) + + try: + redis_config = dict() + for key in self.redis_configuration_properties: + if getattr(self, key, None): + redis_config[underline_to_hyphen(key)] = underline_to_hyphen(getattr(self, key)) + + params = RedisCreateParameters( + location=self.location, + sku=Sku(self.sku['name'].title(), self.sku['size'][0], self.sku['size'][1:]), + tags=self.tags, + redis_configuration=redis_config, + enable_non_ssl_port=self.enable_non_ssl_port, + tenant_settings=self.tenant_settings, + shard_count=self.shard_count, + subnet_id=self.subnet, + static_ip=self.static_ip + ) + + response = self._client.redis.create(resource_group_name=self.resource_group, + name=self.name, + parameters=params) + if isinstance(response, AzureOperationPoller): + response = self.get_poller_result(response) + + except CloudError as exc: + self.log('Error attempting to create the redis cache instance.') + self.fail( + "Error creating the redis cache instance: {0}".format(str(exc))) + return rediscache_to_dict(response) + + def update_rediscache(self): + ''' + Updates redis cache instance with the specified configuration. + + :return: redis cache instance state dictionary + ''' + self.log( + "Updating redis cache instance {0}".format(self.name)) + + try: + redis_config = dict() + for key in self.redis_configuration_properties: + if getattr(self, key, None): + redis_config[underline_to_hyphen(key)] = underline_to_hyphen(getattr(self, key)) + + params = RedisUpdateParameters( + redis_configuration=redis_config, + enable_non_ssl_port=self.enable_non_ssl_port, + tenant_settings=self.tenant_settings, + shard_count=self.shard_count, + sku=Sku(self.sku['name'].title(), self.sku['size'][0], self.sku['size'][1:]), + tags=self.tags + ) + + response = self._client.redis.update(resource_group_name=self.resource_group, + name=self.name, + parameters=params) + if isinstance(response, AzureOperationPoller): + response = self.get_poller_result(response) + + except CloudError as exc: + self.log('Error attempting to update the redis cache instance.') + self.fail( + "Error updating the redis cache instance: {0}".format(str(exc))) + return rediscache_to_dict(response) + + def delete_rediscache(self): + ''' + Deletes specified redis cache instance in the specified subscription and resource group. + + :return: True + ''' + self.log("Deleting the redis cache instance {0}".format(self.name)) + try: + response = self._client.redis.delete(resource_group_name=self.resource_group, + name=self.name) + except CloudError as e: + self.log('Error attempting to delete the redis cache instance.') + self.fail( + "Error deleting the redis cache instance: {0}".format(str(e))) + return True + + def get_rediscache(self): + ''' + Gets the properties of the specified redis cache instance. + + :return: redis cache instance state dictionary + ''' + self.log("Checking if the redis cache instance {0} is present".format(self.name)) + + response = None + + try: + response = self._client.redis.get(resource_group_name=self.resource_group, + name=self.name) + + self.log("Response : {0}".format(response)) + self.log("Redis cache instance : {0} found".format(response.name)) + return rediscache_to_dict(response) + + except CloudError as ex: + self.log("Didn't find redis cache {0} in resource group {1}".format( + self.name, self.resource_group)) + + return False + + def get_subnet(self): + ''' + Gets the properties of the specified subnet. + + :return: subnet id + ''' + self.log("Checking if the subnet {0} is present".format(self.name)) + + response = None + + try: + response = self.network_client.subnets.get(self.subnet['resource_group'], + self.subnet['virtual_network_name'], + self.subnet['name']) + + self.log("Subnet found : {0}".format(response)) + return response.id + + except CloudError as ex: + self.log("Didn't find subnet {0} in resource group {1}".format( + self.subnet['name'], self.subnet['resource_group'])) + + return False + + def parse_subnet(self): + if isinstance(self.subnet, dict): + if 'virtual_network_name' not in self.subnet or \ + 'name' not in self.subnet: + self.fail("Subnet dict must contains virtual_network_name and name") + if 'resource_group' not in self.subnet: + self.subnet['resource_group'] = self.resource_group + subnet_id = self.get_subnet() + else: + subnet_id = self.subnet + return subnet_id + + +def main(): + """Main execution""" + AzureRMRedisCaches() + + +if __name__ == '__main__': + main() diff --git a/packaging/requirements/requirements-azure.txt b/packaging/requirements/requirements-azure.txt index 4125c3335f..67b0adac00 100644 --- a/packaging/requirements/requirements-azure.txt +++ b/packaging/requirements/requirements-azure.txt @@ -15,8 +15,9 @@ azure-mgmt-marketplaceordering==0.1.0 azure-mgmt-monitor==0.5.2 azure-mgmt-network==2.3.0 azure-mgmt-nspkg==2.0.0 -azure-mgmt-rdbms==1.4.1 +azure-mgmt-redis==5.0.0 azure-mgmt-resource==1.2.2 +azure-mgmt-rdbms==1.4.1 azure-mgmt-sql==0.10.0 azure-mgmt-storage==1.5.0 azure-mgmt-trafficmanager==0.50.0 diff --git a/test/integration/targets/azure_rm_rediscache/aliases b/test/integration/targets/azure_rm_rediscache/aliases new file mode 100644 index 0000000000..8f7a9a2e56 --- /dev/null +++ b/test/integration/targets/azure_rm_rediscache/aliases @@ -0,0 +1,3 @@ +cloud/azure +shippable/azure/group1 +destructive diff --git a/test/integration/targets/azure_rm_rediscache/meta/main.yml b/test/integration/targets/azure_rm_rediscache/meta/main.yml new file mode 100644 index 0000000000..95e1952f98 --- /dev/null +++ b/test/integration/targets/azure_rm_rediscache/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/test/integration/targets/azure_rm_rediscache/tasks/main.yml b/test/integration/targets/azure_rm_rediscache/tasks/main.yml new file mode 100644 index 0000000000..d1f91249f1 --- /dev/null +++ b/test/integration/targets/azure_rm_rediscache/tasks/main.yml @@ -0,0 +1,167 @@ +- name: Fix resource prefix + set_fact: + redis_name: "{{ (resource_group | replace('-','x'))[-8:] }}{{ 1000 | random }}redis" + vnet_name: "{{ (resource_group | replace('-','x'))[-8:] }}{{ 1000 | random }}vnet" + subnet_name: "{{ (resource_group | replace('-','x'))[-8:] }}{{ 1000 | random }}subnet" + run_once: yes + +- name: Create a redis cache (Check Mode) + azure_rm_rediscache: + resource_group: "{{ resource_group }}" + name: "{{ redis_name }}" + sku: + name: basic + size: C1 + check_mode: yes + register: output + +- name: Assert creating redis cache check mode + assert: + that: + - output.changed + +- name: Create a redis cache + azure_rm_rediscache: + resource_group: "{{ resource_group }}" + name: "{{ redis_name }}" + sku: + name: basic + size: C1 + register: output + +- name: Assert creating redis cache check mode + assert: + that: + - output.changed + - output.id + +- name: Update the redis cache (idempotent) + azure_rm_rediscache: + resource_group: "{{ resource_group }}" + name: "{{ redis_name }}" + sku: + name: basic + size: C1 + register: output + +- name: assert output not changed + assert: + that: + - not output.changed + +# creating redis Cache costs about 20 mins async operation, +# need to poll status from Creating to Running, then able to perform updating/deleting operation, +# otherwise, will met error: +# "The resource '' is busy processing a previous update request or is undergoing system maintenance. +# As such, it is currently unable to accept the update request. Please try again later." +# comment out updating/deleting tests for test efficiency. +# - name: Update redis cache +# azure_rm_rediscache: +# resource_group: "{{ resource_group }}" +# name: "{{ redis_name }}" +# sku: +# name: basic +# size: C1 +# enable_non_ssl_port: true +# tags: +# testing: foo +# register: output + +# - name: assert output changed +# assert: +# that: +# - output.changed + +# - name: Update redis cache configuration +# azure_rm_rediscache: +# resource_group: "{{ resource_group }}" +# name: "{{ redis_name }}" +# sku: +# name: basic +# size: C1 +# enable_non_ssl_port: true +# maxmemory_policy: allkeys_lru +# tags: +# testing: foo +# register: output + +# - name: assert output changed +# assert: +# that: +# - output.changed + +# - name: Scale up the redis cache +# azure_rm_rediscache: +# resource_group: "{{ resource_group }}" +# name: "{{ redis_name }}" +# sku: +# name: standard +# size: C1 +# tags: +# testing: foo +# register: output + +# - assert: +# that: +# - output.changed + +# - name: Delete the redis cache (Check Mode) +# azure_rm_rediscache: +# resource_group: "{{ resource_group }}" +# name: "{{ redis_name }}" +# state: absent +# check_mode: yes +# register: output + +# - name: assert deleting redis cache check mode +# assert: +# that: output.changed + +# - name: Delete the redis cache +# azure_rm_rediscache: +# resource_group: "{{ resource_group }}" +# name: "{{ redis_name }}" +# state: absent +# register: output + +# - assert: +# that: +# - output.changed + +- name: Create virtual network + azure_rm_virtualnetwork: + resource_group: "{{ resource_group }}" + name: "{{ vnet_name }}" + address_prefixes: "10.10.0.0/16" + +- name: Add subnet + azure_rm_subnet: + resource_group: "{{ resource_group }}" + name: "{{ subnet_name }}" + address_prefix: "10.10.0.0/24" + virtual_network: "{{ vnet_name }}" + +- name: Create redis with subnet + azure_rm_rediscache: + resource_group: "{{ resource_group }}" + name: "{{ redis_name }}2" + sku: + name: premium + size: P1 + subnet: + name: "{{ subnet_name }}" + virtual_network_name: "{{ vnet_name }}" + register: output + +- name: Assert creating redis cache + assert: + that: + - output.changed + - output.id + +# - name: Delete the redis cache +# azure_rm_rediscache: +# resource_group: "{{ resource_group }}" +# name: "{{ redis_name }}2" +# state: absent +# register: output \ No newline at end of file diff --git a/test/runner/requirements/integration.cloud.azure.txt b/test/runner/requirements/integration.cloud.azure.txt index 4125c3335f..67b0adac00 100644 --- a/test/runner/requirements/integration.cloud.azure.txt +++ b/test/runner/requirements/integration.cloud.azure.txt @@ -15,8 +15,9 @@ azure-mgmt-marketplaceordering==0.1.0 azure-mgmt-monitor==0.5.2 azure-mgmt-network==2.3.0 azure-mgmt-nspkg==2.0.0 -azure-mgmt-rdbms==1.4.1 +azure-mgmt-redis==5.0.0 azure-mgmt-resource==1.2.2 +azure-mgmt-rdbms==1.4.1 azure-mgmt-sql==0.10.0 azure-mgmt-storage==1.5.0 azure-mgmt-trafficmanager==0.50.0