From 8c3dca7e7be7d877f8cec76c0220baa58a3570c5 Mon Sep 17 00:00:00 2001 From: Yuwei Zhou Date: Fri, 29 Mar 2019 11:53:49 +0800 Subject: [PATCH] Add azure servicebus module (#51320) --- lib/ansible/module_utils/azure_rm_common.py | 15 + .../cloud/azure/azure_rm_servicebus.py | 207 +++++++ .../cloud/azure/azure_rm_servicebus_facts.py | 539 ++++++++++++++++++ .../cloud/azure/azure_rm_servicebusqueue.py | 339 +++++++++++ .../azure/azure_rm_servicebussaspolicy.py | 307 ++++++++++ .../cloud/azure/azure_rm_servicebustopic.py | 301 ++++++++++ .../azure_rm_servicebustopicsubscription.py | 318 +++++++++++ packaging/requirements/requirements-azure.txt | 1 + .../targets/azure_rm_servicebus/aliases | 3 + .../targets/azure_rm_servicebus/meta/main.yml | 2 + .../azure_rm_servicebus/tasks/main.yml | 169 ++++++ .../requirements/integration.cloud.azure.txt | 1 + 12 files changed, 2202 insertions(+) create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_servicebus.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_servicebus_facts.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_servicebusqueue.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_servicebussaspolicy.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_servicebustopic.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_servicebustopicsubscription.py create mode 100644 test/integration/targets/azure_rm_servicebus/aliases create mode 100644 test/integration/targets/azure_rm_servicebus/meta/main.yml create mode 100644 test/integration/targets/azure_rm_servicebus/tasks/main.yml diff --git a/lib/ansible/module_utils/azure_rm_common.py b/lib/ansible/module_utils/azure_rm_common.py index eace003da1..1afe99c8bf 100644 --- a/lib/ansible/module_utils/azure_rm_common.py +++ b/lib/ansible/module_utils/azure_rm_common.py @@ -161,6 +161,8 @@ try: from azure.storage.blob import PageBlobService, BlockBlobService from adal.authentication_context import AuthenticationContext from azure.mgmt.sql import SqlManagementClient + from azure.mgmt.servicebus import ServiceBusManagementClient + import azure.mgmt.servicebus.models as ServicebusModel from azure.mgmt.rdbms.postgresql import PostgreSQLManagementClient from azure.mgmt.rdbms.mysql import MySQLManagementClient from azure.mgmt.rdbms.mariadb import MariaDBManagementClient @@ -305,6 +307,7 @@ class AzureRMModuleBase(object): self._monitor_client = None self._resource = None self._log_analytics_client = None + self._servicebus_client = None self.check_mode = self.module.check_mode self.api_profile = self.module.params.get('api_profile') @@ -990,6 +993,18 @@ class AzureRMModuleBase(object): self.log('Getting log analytics models') return LogAnalyticsModels + @property + def servicebus_client(self): + self.log('Getting servicebus client') + if not self._servicebus_client: + self._servicebus_client = self.get_mgmt_svc_client(ServiceBusManagementClient, + base_url=self._cloud_environment.endpoints.resource_manager) + return self._servicebus_client + + @property + def servicebus_models(self): + return ServicebusModel + class AzureRMAuthException(Exception): pass diff --git a/lib/ansible/modules/cloud/azure/azure_rm_servicebus.py b/lib/ansible/modules/cloud/azure/azure_rm_servicebus.py new file mode 100644 index 0000000000..3615eb045d --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_servicebus.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# +# Copyright (c) 2018 Yuwei Zhou, +# +# 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_servicebus +version_added: "2.8" +short_description: Manage Azure Service Bus. +description: + - Create, update or delete an Azure Service Bus namespaces. +options: + resource_group: + description: + - name of resource group. + required: true + name: + description: + - name of the servicebus namespace + required: true + state: + description: + - Assert the state of the route. Use 'present' to create or update and + 'absent' to delete. + default: present + choices: + - absent + - present + location: + description: + - Namespace location. + sku: + description: + - Namespace sku. + choices: + - standard + - basic + - premium + default: + standard + +extends_documentation_fragment: + - azure + - azure_tags + +author: + - "Yuwei Zhou (@yuwzho)" + +''' + +EXAMPLES = ''' +- name: Create a namespace + azure_rm_servicebus: + name: deadbeef + location: eastus +''' +RETURN = ''' +id: + description: Current state of the service bus. + returned: success + type: str +''' + +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase +from ansible.module_utils.common.dict_transformations import _snake_to_camel, _camel_to_snake +from ansible.module_utils._text import to_native +from datetime import datetime, timedelta + + +class AzureRMServiceBus(AzureRMModuleBase): + + 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'), + state=dict(type='str', default='present', choices=['present', 'absent']), + sku=dict(type='str', choices=['basic', 'standard', 'premium'], default='standard') + ) + + self.resource_group = None + self.name = None + self.state = None + self.sku = None + self.location = None + + self.results = dict( + changed=False, + id=None + ) + + super(AzureRMServiceBus, self).__init__(self.module_arg_spec, + supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()): + setattr(self, key, kwargs[key]) + + changed = False + + if not self.location: + resource_group = self.get_resource_group(self.resource_group) + self.location = resource_group.location + + original = self.get() + if self.state == 'present' and not original: + self.check_name() + changed = True + if not self.check_mode: + original = self.create() + elif self.state == 'absent' and original: + changed = True + original = None + if not self.check_mode: + self.delete() + self.results['deleted'] = True + + if original: + self.results = self.to_dict(original) + self.results['changed'] = changed + return self.results + + def check_name(self): + try: + check_name = self.servicebus_client.namespaces.check_name_availability_method(self.name) + if not check_name or not check_name.name_available: + self.fail("Error creating namespace {0} - {1}".format(self.name, check_name.message or str(check_name))) + except Exception as exc: + self.fail("Error creating namespace {0} - {1}".format(self.name, exc.message or str(exc))) + + def create(self): + self.log('Cannot find namespace, creating a one') + try: + sku = self.servicebus_models.SBSku(name=str.capitalize(self.sku)) + poller = self.servicebus_client.namespaces.create_or_update(self.resource_group, + self.name, + self.servicebus_models.SBNamespace(location=self.location, + sku=sku)) + ns = self.get_poller_result(poller) + except Exception as exc: + self.fail('Error creating namespace {0} - {1}'.format(self.name, str(exc.inner_exception) or str(exc))) + return ns + + def delete(self): + try: + self.servicebus_client.namespaces.delete(self.resource_group, self.name) + return True + except Exception as exc: + self.fail("Error deleting route {0} - {1}".format(self.name, str(exc))) + + def get(self): + try: + return self.servicebus_client.namespaces.get(self.resource_group, self.name) + except Exception: + return None + + def to_dict(self, instance): + result = dict() + attribute_map = self.servicebus_models.SBNamespace._attribute_map + for attribute in attribute_map.keys(): + value = getattr(instance, attribute) + if not value: + continue + if isinstance(value, self.servicebus_models.SBSku): + result[attribute] = value.name.lower() + elif isinstance(value, datetime): + result[attribute] = str(value) + elif isinstance(value, str): + result[attribute] = to_native(value) + elif attribute == 'max_size_in_megabytes': + result['max_size_in_mb'] = value + else: + result[attribute] = value + return result + + +def is_valid_timedelta(value): + if value == timedelta(10675199, 10085, 477581): + return None + return value + + +def main(): + AzureRMServiceBus() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_servicebus_facts.py b/lib/ansible/modules/cloud/azure/azure_rm_servicebus_facts.py new file mode 100644 index 0000000000..5cc913d705 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_servicebus_facts.py @@ -0,0 +1,539 @@ +#!/usr/bin/python +# +# Copyright (c) 2018 Yuwei Zhou, +# +# 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_servicebus_facts + +version_added: "2.8" + +short_description: Get servicebus facts. + +description: + - Get facts for a specific servicebus or all servicebus in a resource group or subscription. + +options: + name: + description: + - Limit results to a specific servicebus. + resource_group: + description: + - Limit results in a specific resource group. + tags: + description: + - Limit results by providing a list of tags. Format tags as 'key' or 'key:value'. + namespace: + description: + - Servicebus namespace name. + - A namespace is a scoping container for all messaging components. + - Multiple queues and topics can reside within a single namespace, and namespaces often serve as application containers. + - Required when C(type) is not C(namespace). + type: + description: + - Type of the resource. + choices: + - namespace + - queue + - topic + - subscription + topic: + description: + - Topic name. + - Required when C(type) is C(subscription). + show_sas_policies: + description: + - Whether to show the SAS policies. + - Not support when C(type) is C(subscription). + - Note if enable this option, the facts module will raise two more HTTP call for each resources, need more network overhead. + type: bool +extends_documentation_fragment: + - azure + +author: + - "Yuwei Zhou (@yuwzho)" + +''' + +EXAMPLES = ''' +- name: Get all namespaces under a resource group + azure_rm_servicebus_facts: + resource_group: foo + type: namespace + +- name: Get all topics under a namespace + azure_rm_servicebus_facts: + resource_group: foo + namespace: bar + type: topic + +- name: Get a single queue with SAS policies + azure_rm_servicebus_facts: + resource_group: foo + namespace: bar + type: queue + name: sbqueue + show_sas_policies: true + +- name: Get all subscriptions under a resource group + azure_rm_servicebus_facts: + resource_group: foo + type: subscription + namespace: bar + topic: sbtopic +''' +RETURN = ''' +servicebuses: + description: List of servicebus dicts. + returned: always + type: list + contains: + id: + description: + - Resource Id + type: str + sample: "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/foo/providers/Microsoft.ServiceBus/ + namespaces/bar/topics/baz/subscriptions/qux" + name: + description: + - Resource name + type: str + sample: qux + location: + description: + - The Geo-location where the resource lives. + type: str + sample: eastus + namespace: + description: + - Namespace name of the queue or topic, subscription. + type: str + sample: bar + topic: + description: + - Topic name of a subscription. + type: str + sample: baz + tags: + description: + - Resource tags. + type: str + sample: {env: sandbox} + sku: + description: + - Properties of namespace's sku. + type: str + sample: Standard + provisioning_state: + description: + - Provisioning state of the namespace. + type: str + sample: Succeeded + service_bus_endpoint: + description: + - Endpoint you can use to perform Service Bus operations. + type: str + sample: "https://bar.servicebus.windows.net:443/" + metric_id: + description: + - Identifier for Azure Insights metrics of namespace. + type: str + sample: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX:bar" + type: + description: + - Resource type + - Namespace is a scoping container for all messaging components. + - Queue enables you to store messages until the receiving application is available to receive and process them. + - Topic and subscriptions enable 1:n relationships between publishers and subscribers. + sample: "Microsoft.ServiceBus/Namespaces/Topics" + type: str + created_at: + description: + - Exact time the message was created. + sample: "2019-01-25 02:46:55.543953+00:00" + type: str + updated_at: + description: + - The exact time the message was updated. + type: str + sample: "2019-01-25 02:46:55.543953+00:00" + accessed_at: + description: + - Last time the message was sent, or a request was received, for this topic. + type: str + sample: "2019-01-25 02:46:55.543953+00:00" + subscription_count: + description: + - Number of subscriptions under a topic. + type: int + sample: 1 + count_details: + description: + - Message count deatils. + type: dict + contains: + active_message_count: + description: + - Number of active messages in the queue, topic, or subscription. + type: int + sample: 0 + dead_letter_message_count: + description: + - Number of messages that are dead lettered. + type: int + sample: 0 + scheduled_message_count: + description: + - Number of scheduled messages. + type: int + sample: 0 + transfer_message_count: + description: + - Number of messages transferred to another queue, topic, or subscription. + type: int + sample: 0 + transfer_dead_letter_message_count: + description: + - Number of messages transferred into dead letters. + type: int + sample: 0 + support_ordering: + description: + - Value that indicates whether the topic supports ordering. + type: bool + sample: true + status: + description: + - The status of a messaging entity. + type: str + sample: active + requires_session: + description: + - A value that indicates whether the queue or topic supports the concept of sessions. + type: bool + sample: true + requires_duplicate_detection: + description: + - A value indicating if this queue or topic requires duplicate detection. + type: bool + sample: true + max_size_in_mb: + description: + - Maximum size of the queue or topic in megabytes, which is the size of the memory allocated for the topic. + type: int + sample: 5120 + max_delivery_count: + description: + - The maximum delivery count. + - A message is automatically deadlettered after this number of deliveries. + type: int + sample: 10 + lock_duration_in_seconds: + description: + - ISO 8601 timespan duration of a peek-lock. + - The amount of time that the message is locked for other receivers. + - The maximum value for LockDuration is 5 minutes. + type: int + sample: 60 + forward_to: + description: + - Queue or topic name to forward the messages + type: str + sample: quux + forward_dead_lettered_messages_to: + description: + - Queue or topic name to forward the Dead Letter message + type: str + sample: corge + enable_partitioning: + description: + - Value that indicates whether the queue or topic to be partitioned across multiple message brokers is enabled. + type: bool + sample: true + enable_express: + description: + - Value that indicates whether Express Entities are enabled. + - An express topic holds a message in memory temporarily before writing it to persistent storage. + type: bool + sample: true + enable_batched_operations: + description: + - Value that indicates whether server-side batched operations are enabled. + type: bool + sample: true + duplicate_detection_time_in_seconds: + description: + - ISO 8601 timeSpan structure that defines the duration of the duplicate detection history. + type: int + sample: 600 + default_message_time_to_live_seconds: + description: + - ISO 8061 Default message timespan to live value. + - This is the duration after which the message expires, starting from when the message is sent to Service Bus. + - This is the default value used when TimeToLive is not set on a message itself. + type: int + sample: 0 + dead_lettering_on_message_expiration: + description: + - A value that indicates whether this queue or topic has dead letter support when a message expires. + type: int + sample: 0 + dead_lettering_on_filter_evaluation_exceptions: + description: + - Value that indicates whether a subscription has dead letter support on filter evaluation exceptions. + type: int + sample: 0 + auto_delete_on_idle_in_seconds: + description: + - ISO 8061 timeSpan idle interval after which the queue or topic is automatically deleted. + - The minimum duration is 5 minutes. + type: int + sample: true + size_in_bytes: + description: + - The size of the queue or topic, in bytes. + type: int + sample: 0 + message_count: + description: + - Number of messages. + type: int + sample: 10 + sas_policies: + description: + - Dict of SAS policies. + - Will not be returned until C(show_sas_policy) set + type: dict + sample: '{ + "testpolicy1": { + "id": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/ + foo/providers/Microsoft.ServiceBus/namespaces/bar/queues/qux/authorizationRules/testpolicy1", + "keys": { + "key_name": "testpolicy1", + "primary_connection_string": "Endpoint=sb://bar.servicebus.windows.net/; + SharedAccessKeyName=testpolicy1;SharedAccessKey=XXXXXXXXXXXXXXXXX;EntityPath=qux", + "primary_key": "XXXXXXXXXXXXXXXXX", + "secondary_connection_string": "Endpoint=sb://bar.servicebus.windows.net/; + SharedAccessKeyName=testpolicy1;SharedAccessKey=XXXXXXXXXXXXXXX;EntityPath=qux", + "secondary_key": "XXXXXXXXXXXXXXX" + }, + "name": "testpolicy1", + "rights": "listen_send", + "type": "Microsoft.ServiceBus/Namespaces/Queues/AuthorizationRules" + } + }' +''' + +try: + from msrestazure.azure_exceptions import CloudError +except Exception: + # This is handled in azure_rm_common + pass + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase, azure_id_to_dict +from ansible.module_utils.common.dict_transformations import _camel_to_snake +from ansible.module_utils._text import to_native +from datetime import datetime, timedelta + +duration_spec_map = dict( + default_message_time_to_live='default_message_time_to_live_seconds', + duplicate_detection_history_time_window='duplicate_detection_time_in_seconds', + auto_delete_on_idle='auto_delete_on_idle_in_seconds', + lock_duration='lock_duration_in_seconds' +) + + +def is_valid_timedelta(value): + if value == timedelta(10675199, 10085, 477581): + return None + return value + + +class AzureRMServiceBusFacts(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + name=dict(type='str'), + resource_group=dict(type='str'), + tags=dict(type='list'), + type=dict(type='str', required=True, choices=['namespace', 'topic', 'queue', 'subscription']), + namespace=dict(type='str'), + topic=dict(type='str'), + show_sas_policies=dict(type='bool') + ) + + required_if = [ + ('type', 'subscription', ['topic', 'resource_group', 'namespace']), + ('type', 'topic', ['resource_group', 'namespace']), + ('type', 'queue', ['resource_group', 'namespace']) + ] + + self.results = dict( + changed=False, + servicebuses=[] + ) + + self.name = None + self.resource_group = None + self.tags = None + self.type = None + self.namespace = None + self.topic = None + self.show_sas_policies = None + + super(AzureRMServiceBusFacts, self).__init__(self.module_arg_spec, + supports_tags=False, + required_if=required_if, + facts_module=True) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec: + setattr(self, key, kwargs[key]) + + response = [] + if self.name: + response = self.get_item() + elif self.resource_group: + response = self.list_items() + else: + response = self.list_all_items() + + self.results['servicebuses'] = [self.instance_to_dict(x) for x in response] + return self.results + + def instance_to_dict(self, instance): + result = dict() + instance_type = getattr(self.servicebus_models, 'SB{0}'.format(str.capitalize(self.type))) + attribute_map = instance_type._attribute_map + for attribute in attribute_map.keys(): + value = getattr(instance, attribute) + if attribute_map[attribute]['type'] == 'duration': + if is_valid_timedelta(value): + key = duration_spec_map.get(attribute) or attribute + result[key] = int(value.total_seconds()) + elif attribute == 'status': + result['status'] = _camel_to_snake(value) + elif isinstance(value, self.servicebus_models.MessageCountDetails): + result[attribute] = value.as_dict() + elif isinstance(value, self.servicebus_models.SBSku): + result[attribute] = value.name.lower() + elif isinstance(value, datetime): + result[attribute] = str(value) + elif isinstance(value, str): + result[attribute] = to_native(value) + elif attribute == 'max_size_in_megabytes': + result['max_size_in_mb'] = value + else: + result[attribute] = value + if self.show_sas_policies and self.type != 'subscription': + policies = self.get_auth_rules() + for name in policies.keys(): + policies[name]['keys'] = self.get_sas_key(name) + result['sas_policies'] = policies + if self.namespace: + result['namespace'] = self.namespace + if self.topic: + result['topic'] = self.topic + return result + + def _get_client(self): + return getattr(self.servicebus_client, '{0}s'.format(self.type)) + + def get_item(self): + try: + client = self._get_client() + if self.type == 'namespace': + item = client.get(self.resource_group, self.name) + return [item] if self.has_tags(item.tags, self.tags) else [] + elif self.type == 'subscription': + return [client.get(self.resource_group, self.namespace, self.topic, self.name)] + else: + return [client.get(self.resource_group, self.namespace, self.name)] + except Exception: + pass + return [] + + def list_items(self): + try: + client = self._get_client() + if self.type == 'namespace': + response = client.list_by_resource_group(self.resource_group) + return [x for x in response if self.has_tags(x.tags, self.tags)] + elif self.type == 'subscription': + return client.list_by_topic(self.resource_group, self.namespace, self.topic) + else: + return client.list_by_namespace(self.resource_group, self.namespace) + except CloudError as exc: + self.fail("Failed to list items - {0}".format(str(exc))) + return [] + + def list_all_items(self): + self.log("List all items in subscription") + try: + if self.type != 'namespace': + return [] + response = self.servicebus_client.namespaces.list() + return [x for x in response if self.has_tags(x.tags, self.tags)] + except CloudError as exc: + self.fail("Failed to list all items - {0}".format(str(exc))) + return [] + + def get_auth_rules(self): + result = dict() + try: + client = self._get_client() + if self.type == 'namespace': + rules = client.list_authorization_rules(self.resource_group, self.name) + else: + rules = client.list_authorization_rules(self.resource_group, self.namespace, self.name) + while True: + rule = rules.next() + result[rule.name] = self.policy_to_dict(rule) + except StopIteration: + pass + except Exception as exc: + self.fail('Error when getting SAS policies for {0} {1}: {2}'.format(self.type, self.name, exc.message or str(exc))) + return result + + def get_sas_key(self, name): + try: + client = self._get_client() + if self.type == 'namespace': + return client.list_keys(self.resource_group, self.name, name).as_dict() + else: + return client.list_keys(self.resource_group, self.namespace, self.name, name).as_dict() + except Exception as exc: + self.fail('Error when getting SAS policy {0}\'s key - {1}'.format(name, exc.message or str(exc))) + return None + + def policy_to_dict(self, rule): + result = rule.as_dict() + rights = result['rights'] + if 'Manage' in rights: + result['rights'] = 'manage' + elif 'Listen' in rights and 'Send' in rights: + result['rights'] = 'listen_send' + else: + result['rights'] = rights[0].lower() + return result + + +def main(): + AzureRMServiceBusFacts() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_servicebusqueue.py b/lib/ansible/modules/cloud/azure/azure_rm_servicebusqueue.py new file mode 100644 index 0000000000..0843bf75c4 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_servicebusqueue.py @@ -0,0 +1,339 @@ +#!/usr/bin/python +# +# Copyright (c) 2018 Yuwei Zhou, +# +# 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_servicebusqueue +version_added: "2.8" +short_description: Manage Azure Service Bus queue. +description: + - Create, update or delete an Azure Service Bus queue. +options: + resource_group: + description: + - name of resource group. + required: true + name: + description: + - name of the queue. + required: true + namespace: + description: + - Servicebus namespace name. + - A namespace is a scoping container for all messaging components. + - Multiple queues and topics can reside within a single namespace, and namespaces often serve as application containers. + required: true + state: + description: + - Assert the state of the queue. Use 'present' to create or update and + 'absent' to delete. + default: present + choices: + - absent + - present + auto_delete_on_idle_in_seconds: + description: + - Time idle interval after which a queue is automatically deleted. + - The minimum duration is 5 minutes. + type: int + dead_lettering_on_message_expiration: + description: + - A value that indicates whether a queue has dead letter support when a message expires. + type: bool + default_message_time_to_live_seconds: + description: + - Default message timespan to live value. + - This is the duration after which the message expires, starting from when the message is sent to Service Bus. + - This is the default value used when TimeToLive is not set on a message itself. + type: int + enable_batched_operations: + description: + - Value that indicates whether server-side batched operations are enabled. + type: bool + enable_express: + description: + - Value that indicates whether Express Entities are enabled. + - An express topic or queue holds a message in memory temporarily before writing it to persistent storage. + type: bool + enable_partitioning: + description: + - A value that indicates whether the topic or queue is to be partitioned across multiple message brokers. + type: bool + forward_dead_lettered_messages_to: + description: + - Queue or topic name to forward the Dead Letter message for a queue. + forward_to: + description: + - Queue or topic name to forward the messages for a queue. + lock_duration_in_seconds: + description: + - Timespan duration of a peek-lock. + - The amount of time that the message is locked for other receivers. + - The maximum value for LockDuration is 5 minutes. + type: int + max_delivery_count: + description: + - he maximum delivery count. + - A message is automatically deadlettered after this number of deliveries. + type: int + max_size_in_mb: + description: + - The maximum size of the queue in megabytes, which is the size of memory allocated for the queue. + type: int + requires_duplicate_detection: + description: + - A value indicating if this queue or topic requires duplicate detection. + type: bool + duplicate_detection_time_in_seconds: + description: + - TimeSpan structure that defines the duration of the duplicate detection history. + type: int + requires_session: + description: + - A value that indicates whether the queue supports the concept of sessions. + type: bool + status: + description: + - Status of the entity. + choices: + - active + - disabled + - send_disabled + - receive_disabled + +extends_documentation_fragment: + - azure + - azure_tags + +author: + - "Yuwei Zhou (@yuwzho)" + +''' + +EXAMPLES = ''' +- name: Create a queue + azure_rm_servicebusqueue: + name: subqueue + resource_group: foo + namespace: bar + duplicate_detection_time_in_seconds: 600 +''' +RETURN = ''' +id: + description: Current state of the queue. + returned: success + type: str +''' + +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase +from ansible.module_utils.common.dict_transformations import _snake_to_camel, _camel_to_snake +from ansible.module_utils._text import to_native +from datetime import datetime, timedelta + + +duration_spec_map = dict( + default_message_time_to_live='default_message_time_to_live_seconds', + duplicate_detection_history_time_window='duplicate_detection_time_in_seconds', + auto_delete_on_idle='auto_delete_on_idle_in_seconds', + lock_duration='lock_duration_in_seconds' +) + + +sas_policy_spec = dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + name=dict(type='str', required=True), + regenerate_key=dict(type='bool'), + rights=dict(type='str', choices=['manage', 'listen', 'send', 'listen_send']) +) + + +class AzureRMServiceBusQueue(AzureRMModuleBase): + + 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', default='present', choices=['present', 'absent']), + namespace=dict(type='str', required=True), + auto_delete_on_idle_in_seconds=dict(type='int'), + dead_lettering_on_message_expiration=dict(type='bool'), + default_message_time_to_live_seconds=dict(type='int'), + duplicate_detection_time_in_seconds=dict(type='int'), + enable_batched_operations=dict(type='bool'), + enable_express=dict(type='bool'), + enable_partitioning=dict(type='bool'), + forward_dead_lettered_messages_to=dict(type='str'), + forward_to=dict(type='str'), + lock_duration_in_seconds=dict(type='int'), + max_delivery_count=dict(type='int'), + max_size_in_mb=dict(type='int'), + requires_duplicate_detection=dict(type='bool'), + requires_session=dict(type='bool'), + status=dict(type='str', + choices=['active', 'disabled', 'send_disabled', 'receive_disabled']) + ) + + self.resource_group = None + self.name = None + self.state = None + self.namespace = None + self.location = None + self.type = None + self.subscription_topic_name = None + self.auto_delete_on_idle_in_seconds = None + self.dead_lettering_on_message_expiration = None + self.default_message_time_to_live_seconds = None + self.enable_batched_operations = None + self.enable_express = None + self.enable_partitioning = None + self.forward_dead_lettered_messages_to = None + self.forward_to = None + self.lock_duration_in_seconds = None + self.max_delivery_count = None + self.max_size_in_mb = None + self.requires_duplicate_detection = None + self.status = None + + self.results = dict( + changed=False, + id=None + ) + + super(AzureRMServiceBusQueue, self).__init__(self.module_arg_spec, + supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()): + setattr(self, key, kwargs[key]) + + changed = False + + original = self.get() + if self.state == 'present': + # Create the resource instance + params = dict( + dead_lettering_on_message_expiration=self.dead_lettering_on_message_expiration, + enable_batched_operations=self.enable_batched_operations, + enable_express=self.enable_express, + enable_partitioning=self.enable_partitioning, + forward_dead_lettered_messages_to=self.forward_dead_lettered_messages_to, + forward_to=self.forward_to, + max_delivery_count=self.max_delivery_count, + max_size_in_megabytes=self.max_size_in_mb + ) + if self.status: + params['status'] = self.servicebus_models.EntityStatus(str.capitalize(_snake_to_camel(self.status))) + for k, v in duration_spec_map.items(): + seconds = getattr(self, v) + if seconds: + params[k] = timedelta(seconds=seconds) + + instance = self.servicebus_models.SBQueue(**params) + result = original + if not original: + changed = True + result = instance + else: + result = original + attribute_map = set(self.servicebus_models.SBQueue._attribute_map.keys()) - set(self.servicebus_models.SBQueue._validation.keys()) + for attribute in attribute_map: + value = getattr(instance, attribute) + if value and value != getattr(original, attribute): + changed = True + if changed and not self.check_mode: + result = self.create_or_update(instance) + self.results = self.to_dict(result) + elif original: + changed = True + if not self.check_mode: + self.delete() + self.results['deleted'] = True + + self.results['changed'] = changed + return self.results + + def create_or_update(self, param): + try: + client = self._get_client() + return client.create_or_update(self.resource_group, self.namespace, self.name, param) + except Exception as exc: + self.fail('Error creating or updating queue {0} - {1}'.format(self.name, str(exc.inner_exception) or str(exc))) + + def delete(self): + try: + client = self._get_client() + client.delete(self.resource_group, self.namespace, self.name) + return True + except Exception as exc: + self.fail("Error deleting queue {0} - {1}".format(self.name, str(exc))) + + def _get_client(self): + return self.servicebus_client.queues + + def get(self): + try: + client = self._get_client() + return client.get(self.resource_group, self.namespace, self.name) + except Exception: + return None + + def to_dict(self, instance): + result = dict() + attribute_map = self.servicebus_models.SBQueue._attribute_map + for attribute in attribute_map.keys(): + value = getattr(instance, attribute) + if not value: + continue + if attribute_map[attribute]['type'] == 'duration': + if is_valid_timedelta(value): + key = duration_spec_map.get(attribute) or attribute + result[key] = int(value.total_seconds()) + elif attribute == 'status': + result['status'] = _camel_to_snake(value) + elif isinstance(value, self.servicebus_models.MessageCountDetails): + result[attribute] = value.as_dict() + elif isinstance(value, self.servicebus_models.SBSku): + result[attribute] = value.name.lower() + elif isinstance(value, datetime): + result[attribute] = str(value) + elif isinstance(value, str): + result[attribute] = to_native(value) + elif attribute == 'max_size_in_megabytes': + result['max_size_in_mb'] = value + else: + result[attribute] = value + return result + + +def is_valid_timedelta(value): + if value == timedelta(10675199, 10085, 477581): + return None + return value + + +def main(): + AzureRMServiceBusQueue() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_servicebussaspolicy.py b/lib/ansible/modules/cloud/azure/azure_rm_servicebussaspolicy.py new file mode 100644 index 0000000000..e68dbcc2b5 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_servicebussaspolicy.py @@ -0,0 +1,307 @@ +#!/usr/bin/python +# +# Copyright (c) 2018 Yuwei Zhou, +# +# 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_servicebussaspolicy +version_added: "2.8" +short_description: Manage Azure Service Bus SAS policy. +description: + - Create, update or delete an Azure Service Bus SAS policy. +options: + resource_group: + description: + - name of resource group. + required: true + name: + description: + - name of the sas policy. + required: true + state: + description: + - Assert the state of the route. Use 'present' to create or update and + 'absent' to delete. + default: present + choices: + - absent + - present + namespace: + description: + - Manage SAS policy for a namespace without C(queue) or C(topic) set. + - Manage SAS policy for a queue or topic under this namespace. + required: true + queue: + description: + - Type of the messaging queue. + - Cannot set C(topc) when this field set. + topic: + description: + - Name of the messaging topic. + - Cannot set C(queue) when this field set. + regenerate_primary_key: + description: + - Regenerate the SAS policy primary key. + type: bool + default: False + regenerate_secondary_key: + description: + - Regenerate the SAS policy secondary key. + type: bool + default: False + rights: + description: + - Claim rights of the SAS policy. + required: True + choices: + - manage + - listen + - send + - listen_send + +extends_documentation_fragment: + - azure + - azure_tags + +author: + - "Yuwei Zhou (@yuwzho)" + +''' + +EXAMPLES = ''' +- name: Create a namespace + azure_rm_servicebussaspolicy: + name: deadbeef + queue: qux + namespace: bar + resource_group: foo + rights: send +''' +RETURN = ''' +id: + description: Current state of the SAS policy. + returned: Successed + type: str +keys: + description: Key dict of the SAS policy. + returned: Successed + type: dict + contains: + key_name: + description: Name of the SAS policy. + returned: Successed + type: str + primary_connection_string: + description: Primary connection string. + returned: Successed + type: str + primary_key: + description: Primary key. + returned: Successed + type: str + secondary_key: + description: Secondary key. + returned: Successed + type: str + secondary_connection_string: + description: Secondary connection string. + returned: Successed + type: str +name: + description: Name of the SAS policy. + returned: Successed + type: str +rights: + description: Priviledge of the SAS policy. + returned: Successed + type: str +type: + description: Type of the SAS policy. + returned: Successed + type: str +''' + +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase +from ansible.module_utils.common.dict_transformations import _snake_to_camel, _camel_to_snake +from ansible.module_utils._text import to_native +from datetime import datetime, timedelta + + +class AzureRMServiceBusSASPolicy(AzureRMModuleBase): + + 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', default='present', choices=['present', 'absent']), + namespace=dict(type='str', required=True), + queue=dict(type='str'), + topic=dict(type='str'), + regenerate_primary_key=dict(type='bool', default=False), + regenerate_secondary_key=dict(type='bool', default=False), + rights=dict(type='str', choices=['manage', 'listen', 'send', 'listen_send']) + ) + + mutually_exclusive = [ + ['queue', 'topic'] + ] + + required_if = [('state', 'present', ['rights'])] + + self.resource_group = None + self.name = None + self.state = None + self.namespace = None + self.queue = None + self.topic = None + self.regenerate_primary_key = None + self.regenerate_secondary_key = None + self.rights = None + + self.results = dict( + changed=False, + id=None + ) + + super(AzureRMServiceBusSASPolicy, self).__init__(self.module_arg_spec, + mutually_exclusive=mutually_exclusive, + required_if=required_if, + supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()): + setattr(self, key, kwargs[key]) + + changed = False + + policy = self.get_auth_rule() + if self.state == 'present': + if not policy: # Create a new one + changed = True + if not self.check_mode: + policy = self.create_sas_policy() + else: + changed = changed | self.regenerate_primary_key | self.regenerate_secondary_key + if self.regenerate_primary_key and not self.check_mode: + self.regenerate_sas_key('primary') + if self.regenerate_secondary_key and not self.check_mode: + self.regenerate_sas_key('secondary') + self.results = self.policy_to_dict(policy) + self.results['keys'] = self.get_sas_key() + elif policy: + changed = True + if not self.check_mode: + self.delete_sas_policy() + + self.results['changed'] = changed + return self.results + + def _get_client(self): + if self.queue: + return self.servicebus_client.queues + elif self.topic: + return self.servicebus_client.topics + return self.servicebus_client.namespaces + + # SAS policy + def create_sas_policy(self): + if self.rights == 'listen_send': + rights = ['Listen', 'Send'] + elif self.rights == 'manage': + rights = ['Listen', 'Send', 'Manage'] + else: + rights = [str.capitalize(self.rights)] + try: + client = self._get_client() + if self.queue or self.topic: + rule = client.create_or_update_authorization_rule(self.resource_group, self.namespace, self.queue or self.topic, self.name, rights) + else: + rule = client.create_or_update_authorization_rule(self.resource_group, self.namespace, self.name, rights) + return rule + except Exception as exc: + self.fail('Error when creating or updating SAS policy {0} - {1}'.format(self.name, exc.message or str(exc))) + return None + + def get_auth_rule(self): + rule = None + try: + client = self._get_client() + if self.queue or self.topic: + rule = client.get_authorization_rule(self.resource_group, self.namespace, self.queue or self.topic, self.name) + else: + rule = client.get_authorization_rule(self.resource_group, self.namespace, self.name) + except Exception: + pass + return rule + + def delete_sas_policy(self): + try: + client = self._get_client() + if self.queue or self.topic: + client.delete_authorization_rule(self.resource_group, self.namespace, self.queue or self.topic, self.name) + else: + client.delete_authorization_rule(self.resource_group, self.namespace, self.name) + return True + except Exception as exc: + self.fail('Error when deleting SAS policy {0} - {1}'.format(self.name, exc.message or str(exc))) + + def regenerate_sas_key(self, key_type): + try: + client = self._get_client() + key = str.capitalize(key_type) + 'Key' + if self.queue or self.topic: + client.regenerate_keys(self.resource_group, self.namespace, self.queue or self.topic, self.name, key) + else: + client.regenerate_keys(self.resource_group, self.namespace, self.name, key) + except Exception as exc: + self.fail('Error when generating SAS policy {0}\'s key - {1}'.format(self.name, exc.message or str(exc))) + return None + + def get_sas_key(self): + try: + client = self._get_client() + if self.queue or self.topic: + return client.list_keys(self.resource_group, self.namespace, self.queue or self.topic, self.name).as_dict() + else: + return client.list_keys(self.resource_group, self.namespace, self.name).as_dict() + except Exception: + pass + return None + + def policy_to_dict(self, rule): + result = rule.as_dict() + rights = result['rights'] + if 'Manage' in rights: + result['rights'] = 'manage' + elif 'Listen' in rights and 'Send' in rights: + result['rights'] = 'listen_send' + else: + result['rights'] = rights[0].lower() + return result + + +def main(): + AzureRMServiceBusSASPolicy() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_servicebustopic.py b/lib/ansible/modules/cloud/azure/azure_rm_servicebustopic.py new file mode 100644 index 0000000000..94675dea9b --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_servicebustopic.py @@ -0,0 +1,301 @@ +#!/usr/bin/python +# +# Copyright (c) 2018 Yuwei Zhou, +# +# 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_servicebustopic +version_added: "2.8" +short_description: Manage Azure Service Bus. +description: + - Create, update or delete an Azure Service Bus topics. +options: + resource_group: + description: + - name of resource group. + required: true + name: + description: + - name of the topic. + required: true + namespace: + description: + - Servicebus namespace name. + - A namespace is a scoping container for all messaging components. + - Multipletopics can reside within a single namespace. + required: true + state: + description: + - Assert the state of the topic. Use 'present' to create or update and + 'absent' to delete. + default: present + choices: + - absent + - present + auto_delete_on_idle_in_seconds: + description: + - Time idle interval after which a topic is automatically deleted. + - The minimum duration is 5 minutes. + type: int + default_message_time_to_live_seconds: + description: + - Default message timespan to live value. + - This is the duration after which the message expires, starting from when the message is sent to Service Bus. + - This is the default value used when TimeToLive is not set on a message itself. + type: int + enable_batched_operations: + description: + - Value that indicates whether server-side batched operations are enabled. + type: bool + enable_express: + description: + - Value that indicates whether Express Entities are enabled. + - An express topic holds a message in memory temporarily before writing it to persistent storage. + type: bool + enable_partitioning: + description: + - A value that indicates whether the topic is to be partitioned across multiple message brokers. + type: bool + max_size_in_mb: + description: + - The maximum size of the topic in megabytes, which is the size of memory allocated for the topic. + type: int + requires_duplicate_detection: + description: + - A value indicating if this topic requires duplicate detection. + type: bool + duplicate_detection_time_in_seconds: + description: + - TimeSpan structure that defines the duration of the duplicate detection history. + type: int + support_ordering: + description: + - Value that indicates whether the topic supports ordering. + type: bool + status: + description: + - Status of the entity. + choices: + - active + - disabled + - send_disabled + - receive_disabled + +extends_documentation_fragment: + - azure + - azure_tags + +author: + - "Yuwei Zhou (@yuwzho)" + +''' + +EXAMPLES = ''' +- name: Create a topic + azure_rm_servicebustopic: + name: subtopic + resource_group: foo + namespace: bar + duplicate_detection_time_in_seconds: 600 +''' +RETURN = ''' +id: + description: Current state of the topic. + returned: success + type: str +''' + +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase +from ansible.module_utils.common.dict_transformations import _snake_to_camel, _camel_to_snake +from ansible.module_utils._text import to_native +from datetime import datetime, timedelta + + +duration_spec_map = dict( + default_message_time_to_live='default_message_time_to_live_seconds', + duplicate_detection_history_time_window='duplicate_detection_time_in_seconds', + auto_delete_on_idle='auto_delete_on_idle_in_seconds' +) + + +sas_policy_spec = dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + name=dict(type='str', required=True), + regenerate_key=dict(type='bool'), + rights=dict(type='str', choices=['manage', 'listen', 'send', 'listen_send']) +) + + +class AzureRMServiceBusTopic(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + auto_delete_on_idle_in_seconds=dict(type='int'), + default_message_time_to_live_seconds=dict(type='int'), + duplicate_detection_time_in_seconds=dict(type='int'), + enable_batched_operations=dict(type='bool'), + enable_express=dict(type='bool'), + enable_partitioning=dict(type='bool'), + max_size_in_mb=dict(type='int'), + name=dict(type='str', required=True), + namespace=dict(type='str'), + requires_duplicate_detection=dict(type='bool'), + resource_group=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + status=dict(type='str', + choices=['active', 'disabled', 'send_disabled', 'receive_disabled']), + support_ordering=dict(type='bool') + ) + + self.resource_group = None + self.name = None + self.state = None + self.namespace = None + self.auto_delete_on_idle_in_seconds = None + self.default_message_time_to_live_seconds = None + self.enable_batched_operations = None + self.enable_express = None + self.enable_partitioning = None + self.max_size_in_mb = None + self.requires_duplicate_detection = None + self.status = None + self.support_ordering = None + + self.results = dict( + changed=False, + id=None + ) + + super(AzureRMServiceBusTopic, self).__init__(self.module_arg_spec, + supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()): + setattr(self, key, kwargs[key]) + + changed = False + original = self.get() + if self.state == 'present': + # Create the resource instance + params = dict( + enable_batched_operations=self.enable_batched_operations, + enable_express=self.enable_express, + enable_partitioning=self.enable_partitioning, + max_size_in_megabytes=self.max_size_in_mb, + support_ordering=self.support_ordering + ) + if self.status: + params['status'] = self.servicebus_models.EntityStatus(str.capitalize(_snake_to_camel(self.status))) + for k, v in duration_spec_map.items(): + seconds = getattr(self, v) + if seconds: + params[k] = timedelta(seconds=seconds) + + instance = self.servicebus_models.SBTopic(**params) + result = original + if not original: + changed = True + result = instance + else: + result = original + attribute_map = set(self.servicebus_models.SBTopic._attribute_map.keys()) - set(self.servicebus_models.SBTopic._validation.keys()) + for attribute in attribute_map: + value = getattr(instance, attribute) + if value and value != getattr(original, attribute): + changed = True + if changed and not self.check_mode: + result = self.create_or_update(instance) + self.results = self.to_dict(result) + elif original: + changed = True + if not self.check_mode: + self.delete() + self.results['deleted'] = True + + self.results['changed'] = changed + return self.results + + def create_or_update(self, param): + try: + client = self._get_client() + return client.create_or_update(self.resource_group, self.namespace, self.name, param) + except Exception as exc: + self.fail('Error creating or updating topic {0} - {1}'.format(self.name, str(exc.inner_exception) or str(exc))) + + def delete(self): + try: + client = self._get_client() + client.delete(self.resource_group, self.namespace, self.name) + return True + except Exception as exc: + self.fail("Error deleting topic {0} - {1}".format(self.name, str(exc))) + + def _get_client(self): + return self.servicebus_client.topics + + def get(self): + try: + client = self._get_client() + return client.get(self.resource_group, self.namespace, self.name) + except Exception: + return None + + def to_dict(self, instance): + result = dict() + attribute_map = self.servicebus_models.SBTopic._attribute_map + for attribute in attribute_map.keys(): + value = getattr(instance, attribute) + if not value: + continue + if attribute_map[attribute]['type'] == 'duration': + if is_valid_timedelta(value): + key = duration_spec_map.get(attribute) or attribute + result[key] = int(value.total_seconds()) + elif attribute == 'status': + result['status'] = _camel_to_snake(value) + elif isinstance(value, self.servicebus_models.MessageCountDetails): + result[attribute] = value.as_dict() + elif isinstance(value, self.servicebus_models.SBSku): + result[attribute] = value.name.lower() + elif isinstance(value, datetime): + result[attribute] = str(value) + elif isinstance(value, str): + result[attribute] = to_native(value) + elif attribute == 'max_size_in_megabytes': + result['max_size_in_mb'] = value + else: + result[attribute] = value + return result + + +def is_valid_timedelta(value): + if value == timedelta(10675199, 10085, 477581): + return None + return value + + +def main(): + AzureRMServiceBusTopic() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_servicebustopicsubscription.py b/lib/ansible/modules/cloud/azure/azure_rm_servicebustopicsubscription.py new file mode 100644 index 0000000000..82378fbd94 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_servicebustopicsubscription.py @@ -0,0 +1,318 @@ +#!/usr/bin/python +# +# Copyright (c) 2018 Yuwei Zhou, +# +# 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_servicebustopicsubscription +version_added: "2.8" +short_description: Manage Azure Service Bus subscription. +description: + - Create, update or delete an Azure Service Bus subscriptions. +options: + resource_group: + description: + - name of resource group. + required: true + name: + description: + - name of the servicebus subscription. + required: true + state: + description: + - Assert the state of the servicebus subscription. Use 'present' to create or update and + 'absent' to delete. + default: present + choices: + - absent + - present + namespace: + description: + - Servicebus namespace name. + - A namespace is a scoping container for all messaging components. + - Multiple subscriptions and topics can reside within a single namespace, and namespaces often serve as application containers. + required: true + topic: + description: + - Topic name which the subscription subscribe to. + required: true + auto_delete_on_idle_in_seconds: + description: + - Time idle interval after which a subscription is automatically deleted. + - The minimum duration is 5 minutes. + type: int + dead_lettering_on_message_expiration: + description: + - A value that indicates whether a subscription has dead letter support when a message expires. + type: bool + dead_lettering_on_filter_evaluation_exceptions: + description: + - Value that indicates whether a subscription has dead letter support on filter evaluation exceptions. + type: bool + default_message_time_to_live_seconds: + description: + - Default message timespan to live value. + - This is the duration after which the message expires, starting from when the message is sent to Service Bus. + - This is the default value used when TimeToLive is not set on a message itself. + type: int + enable_batched_operations: + description: + - Value that indicates whether server-side batched operations are enabled. + type: bool + forward_dead_lettered_messages_to: + description: + - Queue or topic name to forward the Dead Letter message for a subscription. + forward_to: + description: + - Queue or topic name to forward the messages for a subscription. + lock_duration_in_seconds: + description: + - Timespan duration of a peek-lock. + - The amount of time that the message is locked for other receivers. + - The maximum value for LockDuration is 5 minutes. + type: int + max_delivery_count: + description: + - he maximum delivery count. + - A message is automatically deadlettered after this number of deliveries. + type: int + requires_session: + description: + - A value that indicates whether the subscription supports the concept of sessions. + type: bool + duplicate_detection_time_in_seconds: + description: + - TimeSpan structure that defines the duration of the duplicate detection history. + type: int + status: + description: + - Status of the entity. + choices: + - active + - disabled + - send_disabled + - receive_disabled + +extends_documentation_fragment: + - azure + - azure_tags + +author: + - "Yuwei Zhou (@yuwzho)" + +''' + +EXAMPLES = ''' +- name: Create a subscription + azure_rm_servicebustopicsubscription: + name: sbsub + resource_group: foo + namespace: bar + topic: subtopic +''' +RETURN = ''' +id: + description: Current state of the subscription. + returned: success + type: str +''' + +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase +from ansible.module_utils.common.dict_transformations import _snake_to_camel, _camel_to_snake +from ansible.module_utils._text import to_native +from datetime import datetime, timedelta + + +duration_spec_map = dict( + default_message_time_to_live='default_message_time_to_live_seconds', + duplicate_detection_history_time_window='duplicate_detection_time_in_seconds', + auto_delete_on_idle='auto_delete_on_idle_in_seconds', + lock_duration='lock_duration_in_seconds' +) + + +class AzureRMServiceSubscription(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + auto_delete_on_idle_in_seconds=dict(type='int'), + dead_lettering_on_filter_evaluation_exceptions=dict(type='bool'), + dead_lettering_on_message_expiration=dict(type='bool'), + default_message_time_to_live_seconds=dict(type='int'), + duplicate_detection_time_in_seconds=dict(type='int'), + enable_batched_operations=dict(type='bool'), + forward_dead_lettered_messages_to=dict(type='str'), + forward_to=dict(type='str'), + lock_duration_in_seconds=dict(type='int'), + max_delivery_count=dict(type='int'), + name=dict(type='str', required=True), + namespace=dict(type='str', required=True), + requires_session=dict(type='bool'), + resource_group=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + status=dict(type='str', + choices=['active', 'disabled', 'send_disabled', 'receive_disabled']), + topic=dict(type='str', required=True) + ) + + self.auto_delete_on_idle_in_seconds = None + self.dead_lettering_on_filter_evaluation_exceptions = None + self.dead_lettering_on_message_expiration = None + self.default_message_time_to_live_seconds = None + self.duplicate_detection_time_in_seconds = None + self.enable_batched_operations = None + self.forward_dead_lettered_messages_to = None + self.forward_to = None + self.lock_duration_in_seconds = None + self.max_delivery_count = None + self.name = None + self.namespace = None + self.requires_session = None + self.resource_group = None + self.state = None + self.status = None + self.topic = None + + self.results = dict( + changed=False, + id=None + ) + + super(AzureRMServiceSubscription, self).__init__(self.module_arg_spec, + supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()): + setattr(self, key, kwargs[key]) + + changed = False + + original = self.get() + if self.state == 'present': + # Create the resource instance + params = dict( + dead_lettering_on_filter_evaluation_exceptions=self.dead_lettering_on_filter_evaluation_exceptions, + dead_lettering_on_message_expiration=self.dead_lettering_on_message_expiration, + enable_batched_operations=self.enable_batched_operations, + forward_dead_lettered_messages_to=self.forward_dead_lettered_messages_to, + forward_to=self.forward_to, + max_delivery_count=self.max_delivery_count, + requires_session=self.requires_session + ) + if self.status: + params['status'] = self.servicebus_models.EntityStatus(str.capitalize(_snake_to_camel(self.status))) + for k, v in duration_spec_map.items(): + seconds = getattr(self, v) + if seconds: + params[k] = timedelta(seconds=seconds) + + instance = self.servicebus_models.SBSubscription(**params) + result = original + if not original: + changed = True + result = instance + else: + result = original + attribute_map_keys = set(self.servicebus_models.SBSubscription._attribute_map.keys()) + validation_keys = set(self.servicebus_models.SBSubscription._validation.keys()) + attribute_map = attribute_map_keys - validation_keys + for attribute in attribute_map: + value = getattr(instance, attribute) + if value and value != getattr(original, attribute): + changed = True + if changed and not self.check_mode: + result = self.create_or_update(instance) + self.results = self.to_dict(result) + elif original: + changed = True + if not self.check_mode: + self.delete() + self.results['deleted'] = True + + self.results['changed'] = changed + return self.results + + def create_or_update(self, param): + try: + client = self._get_client() + return client.create_or_update(self.resource_group, self.namespace, self.topic, self.name, param) + except Exception as exc: + self.fail("Error creating or updating servicebus subscription {0} - {1}".format(self.name, str(exc))) + + def delete(self): + try: + client = self._get_client() + client.delete(self.resource_group, self.namespace, self.topic, self.name) + return True + except Exception as exc: + self.fail("Error deleting servicebus subscription {0} - {1}".format(self.name, str(exc))) + + def _get_client(self): + return self.servicebus_client.subscriptions + + def get(self): + try: + client = self._get_client() + return client.get(self.resource_group, self.namespace, self.topic, self.name) + except Exception: + return None + + def to_dict(self, instance): + result = dict() + attribute_map = self.servicebus_models.SBSubscription._attribute_map + for attribute in attribute_map.keys(): + value = getattr(instance, attribute) + if not value: + continue + if attribute_map[attribute]['type'] == 'duration': + if is_valid_timedelta(value): + key = duration_spec_map.get(attribute) or attribute + result[key] = int(value.total_seconds()) + elif attribute == 'status': + result['status'] = _camel_to_snake(value) + elif isinstance(value, self.servicebus_models.MessageCountDetails): + result[attribute] = value.as_dict() + elif isinstance(value, self.servicebus_models.SBSku): + result[attribute] = value.name.lower() + elif isinstance(value, datetime): + result[attribute] = str(value) + elif isinstance(value, str): + result[attribute] = to_native(value) + elif attribute == 'max_size_in_megabytes': + result['max_size_in_mb'] = value + else: + result[attribute] = value + return result + + +def is_valid_timedelta(value): + if value == timedelta(10675199, 10085, 477581): + return None + return value + + +def main(): + AzureRMServiceSubscription() + + +if __name__ == '__main__': + main() diff --git a/packaging/requirements/requirements-azure.txt b/packaging/requirements/requirements-azure.txt index d0eb4fadc9..7c6d4ac956 100644 --- a/packaging/requirements/requirements-azure.txt +++ b/packaging/requirements/requirements-azure.txt @@ -19,6 +19,7 @@ azure-mgmt-nspkg==2.0.0 azure-mgmt-redis==5.0.0 azure-mgmt-resource==2.1.0 azure-mgmt-rdbms==1.4.1 +azure-mgmt-servicebus==0.5.3 azure-mgmt-sql==0.10.0 azure-mgmt-storage==3.1.0 azure-mgmt-trafficmanager==0.50.0 diff --git a/test/integration/targets/azure_rm_servicebus/aliases b/test/integration/targets/azure_rm_servicebus/aliases new file mode 100644 index 0000000000..239e365779 --- /dev/null +++ b/test/integration/targets/azure_rm_servicebus/aliases @@ -0,0 +1,3 @@ +cloud/azure +shippable/azure/group4 +destructive diff --git a/test/integration/targets/azure_rm_servicebus/meta/main.yml b/test/integration/targets/azure_rm_servicebus/meta/main.yml new file mode 100644 index 0000000000..95e1952f98 --- /dev/null +++ b/test/integration/targets/azure_rm_servicebus/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/test/integration/targets/azure_rm_servicebus/tasks/main.yml b/test/integration/targets/azure_rm_servicebus/tasks/main.yml new file mode 100644 index 0000000000..c12d635a1d --- /dev/null +++ b/test/integration/targets/azure_rm_servicebus/tasks/main.yml @@ -0,0 +1,169 @@ +- name: Prepare random number + set_fact: + rpfx: "{{ resource_group | hash('md5') | truncate(7, True, '') }}{{ 1000 | random }}" + run_once: yes + +- name: Create a namespace + azure_rm_servicebus: + name: "ns{{ rpfx }}" + resource_group: "{{ resource_group }}" + register: namespace + +- assert: + that: + - namespace.id + - namespace.changed + +- name: Create a namespace (idempontent) + azure_rm_servicebus: + name: "ns{{ rpfx }}" + resource_group: "{{ resource_group }}" + register: namespace + +- assert: + that: + - not namespace.changed + +- name: Create a queue + azure_rm_servicebusqueue: + name: "queue{{ rpfx }}" + namespace: "ns{{ rpfx }}" + resource_group: "{{ resource_group }}" + register: queue + +- assert: + that: + - queue.id + - queue.changed + +- name: Create a topic (check mode) + azure_rm_servicebustopic: + name: "topic{{ rpfx }}" + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + duplicate_detection_time_in_seconds: 600 + check_mode: yes + register: output + +- assert: + that: + - output.changed + +- name: Create a topic + azure_rm_servicebustopic: + name: "topic{{ rpfx }}" + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + duplicate_detection_time_in_seconds: 600 + register: output + +- assert: + that: + - output.changed + - output.id + - "'subscription_count' not in output" + +- name: Create a topic (idempontent) + azure_rm_servicebustopic: + name: "topic{{ rpfx }}" + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + duplicate_detection_time_in_seconds: 600 + register: output + +- assert: + that: + - not output.changed + +- name: Create test policy + azure_rm_servicebussaspolicy: + name: testpolicy + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + topic: "topic{{ rpfx }}" + rights: manage + +- name: Create a subscription + azure_rm_servicebustopicsubscription: + name: "subs{{ rpfx }}" + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + topic: "topic{{ rpfx }}" + register: subs + +- assert: + that: + - subs.id + - subs.changed + +- name: Retrive topic + azure_rm_servicebus_facts: + type: topic + name: "topic{{ rpfx }}" + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + show_sas_policies: yes + register: facts + +- assert: + that: + - "facts.servicebuses | length == 1" + - facts.servicebuses[0].id == output.id + - facts.servicebuses[0].subscription_count == 1 + - facts.servicebuses[0].sas_policies.testpolicy + - facts.servicebuses[0].sas_policies.testpolicy.rights == 'manage' + +- name: Delete subscription + azure_rm_servicebustopicsubscription: + name: "subs{{ rpfx }}" + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + topic: "topic{{ rpfx }}" + state: absent + +- name: Retrive topic + azure_rm_servicebus_facts: + type: topic + name: "topic{{ rpfx }}" + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + show_sas_policies: yes + register: facts + +- assert: + that: + - facts.servicebuses[0].subscription_count == 0 + - "facts.servicebuses | length == 1" + +- name: Delete topic + azure_rm_servicebustopic: + name: "topic{{ rpfx }}" + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + state: absent + +- name: Retrive topic + azure_rm_servicebus_facts: + name: "topic{{ rpfx }}" + type: topic + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + show_sas_policies: yes + register: facts + +- assert: + that: + - "facts.servicebuses | length == 0" + +- name: Delete queue + azure_rm_servicebusqueue: + name: "queue{{ rpfx }}" + resource_group: "{{ resource_group }}" + namespace: "ns{{ rpfx }}" + state: absent + +- name: Delete namespace + azure_rm_servicebus: + name: "ns{{ rpfx }}" + resource_group: "{{ resource_group }}" + state: absent \ 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 d0eb4fadc9..7c6d4ac956 100644 --- a/test/runner/requirements/integration.cloud.azure.txt +++ b/test/runner/requirements/integration.cloud.azure.txt @@ -19,6 +19,7 @@ azure-mgmt-nspkg==2.0.0 azure-mgmt-redis==5.0.0 azure-mgmt-resource==2.1.0 azure-mgmt-rdbms==1.4.1 +azure-mgmt-servicebus==0.5.3 azure-mgmt-sql==0.10.0 azure-mgmt-storage==3.1.0 azure-mgmt-trafficmanager==0.50.0