From b4732dd2e6db5ff193a8b013ebe73a56f7ca373d Mon Sep 17 00:00:00 2001 From: Yuwei Zhou Date: Tue, 27 Aug 2019 13:18:28 +0800 Subject: [PATCH] Support Azure IoT hub and related module (#55121) * Support iothub creation * raise errordetailexception rather than clouderror * add facts * change requirement * compare endpoint * add documentation * add documentation * add iot device facts * modify line ending * add auth method * add iot module * add consumer group * add the test * enhencement of doc * add list consumer groups * fix lint * fix lint * fix doc * fix doc * Update auzre_rm_iothub related document * changed paramter's type * update type * rename facts -> info * fixed sanity * missed during merge --- lib/ansible/module_utils/azure_rm_common.py | 99 +- .../modules/cloud/azure/azure_rm_iotdevice.py | 472 +++++++++ .../cloud/azure/azure_rm_iotdevice_info.py | 313 ++++++ .../cloud/azure/azure_rm_iotdevicemodule.py | 379 ++++++++ .../modules/cloud/azure/azure_rm_iothub.py | 896 ++++++++++++++++++ .../cloud/azure/azure_rm_iothub_info.py | 618 ++++++++++++ .../azure/azure_rm_iothubconsumergroup.py | 169 ++++ packaging/requirements/requirements-azure.txt | 1 + .../targets/azure_rm_iothub/aliases | 3 + .../targets/azure_rm_iothub/meta/main.yml | 2 + .../targets/azure_rm_iothub/tasks/main.yml | 172 ++++ .../requirements/integration.cloud.azure.txt | 1 + 12 files changed, 3118 insertions(+), 7 deletions(-) create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_iotdevice.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_iotdevice_info.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_iotdevicemodule.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_iothub.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_iothub_info.py create mode 100644 lib/ansible/modules/cloud/azure/azure_rm_iothubconsumergroup.py create mode 100644 test/integration/targets/azure_rm_iothub/aliases create mode 100644 test/integration/targets/azure_rm_iothub/meta/main.yml create mode 100644 test/integration/targets/azure_rm_iothub/tasks/main.yml diff --git a/lib/ansible/module_utils/azure_rm_common.py b/lib/ansible/module_utils/azure_rm_common.py index 20e19629dec..eede56bf774 100644 --- a/lib/ansible/module_utils/azure_rm_common.py +++ b/lib/ansible/module_utils/azure_rm_common.py @@ -172,10 +172,26 @@ try: import azure.mgmt.loganalytics.models as LogAnalyticsModels from azure.mgmt.automation import AutomationClient import azure.mgmt.automation.models as AutomationModel + from azure.mgmt.iothub import IotHubClient + from azure.mgmt.iothub import models as IoTHubModels + from msrest.service_client import ServiceClient + from msrestazure import AzureConfiguration + from msrest.authentication import Authentication except ImportError as exc: + Authentication = object HAS_AZURE_EXC = traceback.format_exc() HAS_AZURE = False +from base64 import b64encode, b64decode +from hashlib import sha256 +from hmac import HMAC +from time import time + +try: + from urllib import (urlencode, quote_plus) +except ImportError: + from urllib.parse import (urlencode, quote_plus) + try: from azure.cli.core.util import CLIError from azure.common.credentials import get_azure_cli_credentials, get_cli_profile @@ -311,6 +327,7 @@ class AzureRMModuleBase(object): self._log_analytics_client = None self._servicebus_client = None self._automation_client = None + self._IoThub_client = None self.check_mode = self.module.check_mode self.api_profile = self.module.params.get('api_profile') @@ -771,19 +788,48 @@ class AzureRMModuleBase(object): setattr(client, '_ansible_models', importlib.import_module(client_type.__module__).models) client.models = types.MethodType(_ansible_get_models, client) + client.config = self.add_user_agent(client.config) + + if self.azure_auth._cert_validation_mode == 'ignore': + client.config.session_configuration_callback = self._validation_ignore_callback + + return client + + def add_user_agent(self, config): # Add user agent for Ansible - client.config.add_user_agent(ANSIBLE_USER_AGENT) + config.add_user_agent(ANSIBLE_USER_AGENT) # Add user agent when running from Cloud Shell if CLOUDSHELL_USER_AGENT_KEY in os.environ: - client.config.add_user_agent(os.environ[CLOUDSHELL_USER_AGENT_KEY]) + config.add_user_agent(os.environ[CLOUDSHELL_USER_AGENT_KEY]) # Add user agent when running from VSCode extension if VSCODEEXT_USER_AGENT_KEY in os.environ: - client.config.add_user_agent(os.environ[VSCODEEXT_USER_AGENT_KEY]) - - if self.azure_auth._cert_validation_mode == 'ignore': - client.config.session_configuration_callback = self._validation_ignore_callback + config.add_user_agent(os.environ[VSCODEEXT_USER_AGENT_KEY]) + return config + + def generate_sas_token(self, **kwags): + base_url = kwags.get('base_url', None) + expiry = kwags.get('expiry', time() + 3600) + key = kwags.get('key', None) + policy = kwags.get('policy', None) + url = quote_plus(base_url) + ttl = int(expiry) + sign_key = '{0}\n{1}'.format(url, ttl) + signature = b64encode(HMAC(b64decode(key), sign_key.encode('utf-8'), sha256).digest()) + result = { + 'sr': url, + 'sig': signature, + 'se': str(ttl), + } + if policy: + result['skn'] = policy + return 'SharedAccessSignature ' + urlencode(result) - return client + def get_data_svc_client(self, **kwags): + url = kwags.get('base_url', None) + config = AzureConfiguration(base_url='https://{0}'.format(url)) + config.credentials = AzureSASAuthentication(token=self.generate_sas_token(**kwags)) + config = self.add_user_agent(config) + return ServiceClient(creds=config.credentials, config=config) # passthru methods to AzureAuth instance for backcompat @property @@ -1020,6 +1066,45 @@ class AzureRMModuleBase(object): def automation_models(self): return AutomationModel + @property + def IoThub_client(self): + self.log('Getting iothub client') + if not self._IoThub_client: + self._IoThub_client = self.get_mgmt_svc_client(IotHubClient, + base_url=self._cloud_environment.endpoints.resource_manager) + return self._IoThub_client + + @property + def IoThub_models(self): + return IoTHubModels + + +class AzureSASAuthentication(Authentication): + """Simple SAS Authentication. + An implementation of Authentication in + https://github.com/Azure/msrest-for-python/blob/0732bc90bdb290e5f58c675ffdd7dbfa9acefc93/msrest/authentication.py + + :param str token: SAS token + """ + def __init__(self, token): + self.token = token + + def signed_session(self): + session = super(AzureSASAuthentication, self).signed_session() + session.headers['Authorization'] = self.token + return session + + def automation_client(self): + self.log('Getting automation client') + if not self._automation_client: + self._automation_client = self.get_mgmt_svc_client(AutomationClient, + base_url=self._cloud_environment.endpoints.resource_manager) + return self._automation_client + + @property + def automation_models(self): + return AutomationModel + class AzureRMAuthException(Exception): pass diff --git a/lib/ansible/modules/cloud/azure/azure_rm_iotdevice.py b/lib/ansible/modules/cloud/azure/azure_rm_iotdevice.py new file mode 100644 index 00000000000..86698c23421 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_iotdevice.py @@ -0,0 +1,472 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 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_iotdevice +version_added: "2.9" +short_description: Manage Azure IoT hub device +description: + - Create, delete an Azure IoT hub device. +options: + hub: + description: + - Name of IoT Hub. + type: str + required: true + hub_policy_name: + description: + - Policy name of the IoT Hub which will be used to query from IoT hub. + - This policy should have 'RegistryWrite, ServiceConnect, DeviceConnect' accesses. You may get 401 error when you lack any of these. + type: str + required: true + hub_policy_key: + description: + - Key of the I(hub_policy_name). + type: str + required: true + name: + description: + - Name of the IoT hub device identity. + type: str + required: true + state: + description: + - State of the IoT hub. Use C(present) to create or update an IoT hub device and C(absent) to delete an IoT hub device. + type: str + default: present + choices: + - absent + - present + auth_method: + description: + - The authorization type an entity is to be created with. + type: str + choices: + - sas + - certificate_authority + - self_signed + default: sas + primary_key: + description: + - Explicit self-signed certificate thumbprint to use for primary key. + - Explicit Shared Private Key to use for primary key. + type: str + aliases: + - primary_thumbprint + secondary_key: + description: + - Explicit self-signed certificate thumbprint to use for secondary key. + - Explicit Shared Private Key to use for secondary key. + type: str + aliases: + - secondary_thumbprint + status: + description: + - Set device status upon creation. + type: bool + edge_enabled: + description: + - Flag indicating edge enablement. + - Not supported in IoT Hub with Basic tier. + type: bool + twin_tags: + description: + - A section that the solution back end can read from and write to. + - Tags are not visible to device apps. + - "The tag can be nested dictionary, '.', '$', '#', ' ' is not allowed in the key." + - List is not supported. + - Not supported in IoT Hub with Basic tier. + type: dict + desired: + description: + - Used along with reported properties to synchronize device configuration or conditions. + - "The tag can be nested dictionary, '.', '$', '#', ' ' is not allowed in the key." + - List is not supported. + - Not supported in IoT Hub with Basic tier. + type: dict +extends_documentation_fragment: + - azure + - azure_tags + +author: + - Yuwei Zhou (@yuwzho) + +''' + +EXAMPLES = ''' +- name: Create simplest Azure IoT Hub device + azure_rm_iotdevice: + hub: myHub + name: Testing + hub_policy_name: iothubowner + hub_policy_key: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + +- name: Create Azure IoT Edge device + azure_rm_iotdevice: + hub: myHub + name: Testing + hub_policy_name: iothubowner + hub_policy_key: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + edge_enabled: yes + +- name: Create Azure IoT Hub device with device twin properties and tag + azure_rm_iotdevice: + hub: myHub + name: Testing + hub_policy_name: iothubowner + hub_policy_key: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + twin_tags: + location: + country: US + city: Redmond + sensor: humidity + desired: + period: 100 +''' + +RETURN = ''' +device: + description: + - IoT Hub device. + returned: always + type: dict + sample: { + "authentication": { + "symmetricKey": { + "primaryKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "secondaryKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + }, + "type": "sas", + "x509Thumbprint": { + "primaryThumbprint": null, + "secondaryThumbprint": null + } + }, + "capabilities": { + "iotEdge": false + }, + "changed": true, + "cloudToDeviceMessageCount": 0, + "connectionState": "Disconnected", + "connectionStateUpdatedTime": "0001-01-01T00:00:00", + "deviceId": "Testing", + "etag": "NzA2NjU2ODc=", + "failed": false, + "generationId": "636903014505613307", + "lastActivityTime": "0001-01-01T00:00:00", + "modules": [ + { + "authentication": { + "symmetricKey": { + "primaryKey": "XXXXXXXXXXXXXXXXXXX", + "secondaryKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + }, + "type": "sas", + "x509Thumbprint": { + "primaryThumbprint": null, + "secondaryThumbprint": null + } + }, + "cloudToDeviceMessageCount": 0, + "connectionState": "Disconnected", + "connectionStateUpdatedTime": "0001-01-01T00:00:00", + "deviceId": "testdevice", + "etag": "MjgxOTE5ODE4", + "generationId": "636903840872788074", + "lastActivityTime": "0001-01-01T00:00:00", + "managedBy": null, + "moduleId": "test" + } + ], + "properties": { + "desired": { + "$metadata": { + "$lastUpdated": "2019-04-10T05:00:46.2702079Z", + "$lastUpdatedVersion": 8, + "period": { + "$lastUpdated": "2019-04-10T05:00:46.2702079Z", + "$lastUpdatedVersion": 8 + } + }, + "$version": 1, + "period": 100 + }, + "reported": { + "$metadata": { + "$lastUpdated": "2019-04-08T06:24:10.5613307Z" + }, + "$version": 1 + } + }, + "status": "enabled", + "statusReason": null, + "statusUpdatedTime": "0001-01-01T00:00:00", + "tags": { + "location": { + "country": "us", + "city": "Redmond" + }, + "sensor": "humidity" + } + } +''' # NOQA + +import json +import copy +import re + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase, format_resource_id +from ansible.module_utils.common.dict_transformations import _snake_to_camel + +try: + from msrestazure.tools import parse_resource_id + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMIoTDevice(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + name=dict(type='str', required=True), + hub_policy_name=dict(type='str', required=True), + hub_policy_key=dict(type='str', required=True), + hub=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + status=dict(type='bool'), + edge_enabled=dict(type='bool'), + twin_tags=dict(type='dict'), + desired=dict(type='dict'), + auth_method=dict(type='str', choices=['self_signed', 'sas', 'certificate_authority'], default='sas'), + primary_key=dict(type='str', no_log=True, aliases=['primary_thumbprint']), + secondary_key=dict(type='str', no_log=True, aliases=['secondary_thumbprint']) + ) + + self.results = dict( + changed=False, + id=None + ) + + self.name = None + self.hub = None + self.hub_policy_key = None + self.hub_policy_name = None + self.state = None + self.status = None + self.edge_enabled = None + self.twin_tags = None + self.desired = None + self.auth_method = None + self.primary_key = None + self.secondary_key = None + + required_if = [ + ['auth_method', 'self_signed', ['certificate_authority']] + ] + + self._base_url = None + self._mgmt_client = None + self.query_parameters = { + 'api-version': '2018-06-30' + } + self.header_parameters = { + 'Content-Type': 'application/json; charset=utf-8', + 'accept-language': 'en-US' + } + super(AzureRMIoTDevice, self).__init__(self.module_arg_spec, supports_check_mode=True, required_if=required_if) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec.keys(): + setattr(self, key, kwargs[key]) + + self._base_url = '{0}.azure-devices.net'.format(self.hub) + config = { + 'base_url': self._base_url, + 'key': self.hub_policy_key, + 'policy': self.hub_policy_name + } + self._mgmt_client = self.get_data_svc_client(**config) + + changed = False + + device = self.get_device() + if self.state == 'present': + if not device: + changed = True + auth = {'type': _snake_to_camel(self.auth_method)} + if self.auth_method == 'self_signed': + auth['x509Thumbprint'] = { + 'primaryThumbprint': self.primary_key, + 'secondaryThumbprint': self.secondary_key + } + elif self.auth_method == 'sas': + auth['symmetricKey'] = { + 'primaryKey': self.primary_key, + 'secondaryKey': self.secondary_key + } + device = { + 'deviceId': self.name, + 'capabilities': {'iotEdge': self.edge_enabled or False}, + 'authentication': auth + } + if self.status is not None and not self.status: + device['status'] = 'disabled' + else: + if self.edge_enabled is not None and self.edge_enabled != device['capabilities']['iotEdge']: + changed = True + device['capabilities']['iotEdge'] = self.edge_enabled + if self.status is not None: + status = 'enabled' if self.status else 'disabled' + if status != device['status']: + changed = True + device['status'] = status + if changed and not self.check_mode: + device = self.create_or_update_device(device) + twin = self.get_twin() + if twin: + if not twin.get('tags'): + twin['tags'] = dict() + twin_change = False + if self.twin_tags and not self.is_equal(self.twin_tags, twin['tags']): + twin_change = True + if self.desired and not self.is_equal(self.desired, twin['properties']['desired']): + twin_change = True + if twin_change and not self.check_mode: + self.update_twin(twin) + changed = changed or twin_change + device['tags'] = twin.get('tags') or dict() + device['properties'] = twin['properties'] + device['modules'] = self.list_device_modules() + elif self.twin_tags or self.desired: + self.fail("Device twin is not supported in IoT Hub with basic tier.") + elif device: + if not self.check_mode: + self.delete_device(device['etag']) + changed = True + device = None + self.results = device or dict() + self.results['changed'] = changed + return self.results + + def is_equal(self, updated, original): + changed = False + if not isinstance(updated, dict): + self.fail('The Property or Tag should be a dict') + for key in updated.keys(): + if re.search(r'[.|$|#|\s]', key): + self.fail("Property or Tag name has invalid characters: '.', '$', '#' or ' '. Got '{0}'".format(key)) + original_value = original.get(key) + updated_value = updated[key] + if isinstance(updated_value, dict): + if not isinstance(original_value, dict): + changed = True + original[key] = updated_value + elif not self.is_equal(updated_value, original_value): + changed = True + elif original_value != updated_value: + changed = True + original[key] = updated_value + return not changed + + def create_or_update_device(self, device): + try: + url = '/devices/{0}'.format(self.name) + headers = copy.copy(self.header_parameters) + if device.get('etag'): + headers['If-Match'] = '"{0}"'.format(device['etag']) + request = self._mgmt_client.put(url, self.query_parameters) + response = self._mgmt_client.send(request=request, headers=headers, content=device) + if response.status_code not in [200, 201, 202]: + raise CloudError(response) + return json.loads(response.text) + except Exception as exc: + if exc.status_code in [403] and self.edge_enabled: + self.fail('Edge device is not supported in IoT Hub with Basic tier.') + else: + self.fail('Error when creating or updating IoT Hub device {0}: {1}'.format(self.name, exc.message or str(exc))) + + def delete_device(self, etag): + try: + url = '/devices/{0}'.format(self.name) + headers = copy.copy(self.header_parameters) + headers['If-Match'] = '"{0}"'.format(etag) + request = self._mgmt_client.delete(url, self.query_parameters) + response = self._mgmt_client.send(request=request, headers=headers) + if response.status_code not in [204]: + raise CloudError(response) + except Exception as exc: + self.fail('Error when deleting IoT Hub device {0}: {1}'.format(self.name, exc.message or str(exc))) + + def get_device(self): + try: + url = '/devices/{0}'.format(self.name) + device = self._https_get(url, self.query_parameters, self.header_parameters) + return device + except Exception as exc: + if exc.status_code in [404]: + return None + else: + self.fail('Error when getting IoT Hub device {0}: {1}'.format(self.name, exc.message or str(exc))) + + def get_twin(self): + try: + url = '/twins/{0}'.format(self.name) + return self._https_get(url, self.query_parameters, self.header_parameters) + except Exception as exc: + if exc.status_code in [403]: + # The Basic sku has nothing to to with twin + return None + else: + self.fail('Error when getting IoT Hub device {0} twin: {1}'.format(self.name, exc.message or str(exc))) + + def update_twin(self, twin): + try: + url = '/twins/{0}'.format(self.name) + headers = copy.copy(self.header_parameters) + headers['If-Match'] = '"{0}"'.format(twin['etag']) + request = self._mgmt_client.patch(url, self.query_parameters) + response = self._mgmt_client.send(request=request, headers=headers, content=twin) + if response.status_code not in [200]: + raise CloudError(response) + return json.loads(response.text) + except Exception as exc: + self.fail('Error when creating or updating IoT Hub device twin {0}: {1}'.format(self.name, exc.message or str(exc))) + + def list_device_modules(self): + try: + url = '/devices/{0}/modules'.format(self.name) + return self._https_get(url, self.query_parameters, self.header_parameters) + except Exception as exc: + self.fail('Error when listing IoT Hub device {0} modules: {1}'.format(self.name, exc.message or str(exc))) + + def _https_get(self, url, query_parameters, header_parameters): + request = self._mgmt_client.get(url, query_parameters) + response = self._mgmt_client.send(request=request, headers=header_parameters, content=None) + if response.status_code not in [200]: + raise CloudError(response) + return json.loads(response.text) + + +def main(): + AzureRMIoTDevice() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_iotdevice_info.py b/lib/ansible/modules/cloud/azure/azure_rm_iotdevice_info.py new file mode 100644 index 00000000000..a283b68bb54 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_iotdevice_info.py @@ -0,0 +1,313 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 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_iotdevice_info +version_added: "2.9" +short_description: Facts of Azure IoT hub device +description: + - Query, get Azure IoT hub device. +options: + hub: + description: + - Name of IoT Hub. + type: str + required: true + hub_policy_name: + description: + - Policy name of the IoT Hub which will be used to query from IoT hub. + - This policy should have at least 'Registry Read' access. + type: str + required: true + hub_policy_key: + description: + - Key of the I(hub_policy_name). + type: str + required: true + name: + description: + - Name of the IoT hub device identity. + type: str + aliases: + - device_id + module_id: + description: + - Name of the IoT hub device module. + - Must use with I(device_id) defined. + type: str + query: + description: + - Query an IoT hub to retrieve information regarding device twins using a SQL-like language. + - "See U(https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-query-language)." + type: str + top: + description: + - Used when I(name) not defined. + - List the top n devices in the query. + type: int +extends_documentation_fragment: + - azure + - azure_tags + +author: + - Yuwei Zhou (@yuwzho) +''' + +EXAMPLES = ''' +- name: Get the details of a device + azure_rm_iotdevice_info: + name: Testing + hub: MyIoTHub + hub_policy_name: registryRead + hub_policy_key: XXXXXXXXXXXXXXXXXXXX + +- name: Query all device modules in an IoT Hub + azure_rm_iotdevice_info: + query: "SELECT * FROM devices.modules" + hub: MyIoTHub + hub_policy_name: registryRead + hub_policy_key: XXXXXXXXXXXXXXXXXXXX + +- name: List all devices in an IoT Hub + azure_rm_iotdevice_info: + hub: MyIoTHub + hub_policy_name: registryRead + hub_policy_key: XXXXXXXXXXXXXXXXXXXX +''' + +RETURN = ''' +iot_devices: + description: + - IoT Hub device. + returned: always + type: dict + sample: { + "authentication": { + "symmetricKey": { + "primaryKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "secondaryKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + }, + "type": "sas", + "x509Thumbprint": { + "primaryThumbprint": null, + "secondaryThumbprint": null + } + }, + "capabilities": { + "iotEdge": false + }, + "changed": true, + "cloudToDeviceMessageCount": 0, + "connectionState": "Disconnected", + "connectionStateUpdatedTime": "0001-01-01T00:00:00", + "deviceId": "Testing", + "etag": "NzA2NjU2ODc=", + "failed": false, + "generationId": "636903014505613307", + "lastActivityTime": "0001-01-01T00:00:00", + "modules": [ + { + "authentication": { + "symmetricKey": { + "primaryKey": "XXXXXXXXXXXXXXXXXXX", + "secondaryKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + }, + "type": "sas", + "x509Thumbprint": { + "primaryThumbprint": null, + "secondaryThumbprint": null + } + }, + "cloudToDeviceMessageCount": 0, + "connectionState": "Disconnected", + "connectionStateUpdatedTime": "0001-01-01T00:00:00", + "deviceId": "testdevice", + "etag": "MjgxOTE5ODE4", + "generationId": "636903840872788074", + "lastActivityTime": "0001-01-01T00:00:00", + "managedBy": null, + "moduleId": "test" + } + ], + "properties": { + "desired": { + "$metadata": { + "$lastUpdated": "2019-04-10T05:00:46.2702079Z", + "$lastUpdatedVersion": 8, + "period": { + "$lastUpdated": "2019-04-10T05:00:46.2702079Z", + "$lastUpdatedVersion": 8 + } + }, + "$version": 1, + "period": 100 + }, + "reported": { + "$metadata": { + "$lastUpdated": "2019-04-08T06:24:10.5613307Z" + }, + "$version": 1 + } + }, + "status": "enabled", + "statusReason": null, + "statusUpdatedTime": "0001-01-01T00:00:00", + "tags": { + "location": { + "country": "us", + "city": "Redmond" + }, + "sensor": "humidity" + } + } +''' # NOQA + +import json + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase, format_resource_id +from ansible.module_utils.common.dict_transformations import _snake_to_camel, _camel_to_snake + +try: + from msrestazure.tools import parse_resource_id + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMIoTDeviceFacts(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + name=dict(type='str', aliases=['device_id']), + module_id=dict(type='str'), + query=dict(type='str'), + hub=dict(type='str', required=True), + hub_policy_name=dict(type='str', required=True), + hub_policy_key=dict(type='str', required=True), + top=dict(type='int') + ) + + self.results = dict( + changed=False, + iot_devices=[] + ) + + self.name = None + self.module_id = None + self.hub = None + self.hub_policy_name = None + self.hub_policy_key = None + self.top = None + + self._mgmt_client = None + self._base_url = None + self.query_parameters = { + 'api-version': '2018-06-30' + } + self.header_parameters = { + 'Content-Type': 'application/json; charset=utf-8', + 'accept-language': 'en-US' + } + super(AzureRMIoTDeviceFacts, self).__init__(self.module_arg_spec, supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec.keys(): + setattr(self, key, kwargs[key]) + + self._base_url = '{0}.azure-devices.net'.format(self.hub) + config = { + 'base_url': self._base_url, + 'key': self.hub_policy_key, + 'policy': self.hub_policy_name + } + if self.top: + self.query_parameters['top'] = self.top + self._mgmt_client = self.get_data_svc_client(**config) + + response = [] + if self.module_id: + response = [self.get_device_module()] + elif self.name: + response = [self.get_device()] + elif self.query: + response = self.hub_query() + else: + response = self.list_devices() + + self.results['iot_devices'] = response + return self.results + + def hub_query(self): + try: + url = '/devices/query' + request = self._mgmt_client.post(url, self.query_parameters) + query = { + 'query': self.query + } + response = self._mgmt_client.send(request=request, headers=self.header_parameters, content=query) + if response.status_code not in [200]: + raise CloudError(response) + return json.loads(response.text) + except Exception as exc: + self.fail('Error when running query "{0}" in IoT Hub {1}: {2}'.format(self.query, self.hub, exc.message or str(exc))) + + def get_device(self): + try: + url = '/devices/{0}'.format(self.name) + device = self._https_get(url, self.query_parameters, self.header_parameters) + device['modules'] = self.list_device_modules() + return device + except Exception as exc: + self.fail('Error when getting IoT Hub device {0}: {1}'.format(self.name, exc.message or str(exc))) + + def get_device_module(self): + try: + url = '/devices/{0}/modules/{1}'.format(self.name, self.module_id) + return self._https_get(url, self.query_parameters, self.header_parameters) + except Exception as exc: + self.fail('Error when getting IoT Hub device {0}: {1}'.format(self.name, exc.message or str(exc))) + + def list_device_modules(self): + try: + url = '/devices/{0}/modules'.format(self.name) + return self._https_get(url, self.query_parameters, self.header_parameters) + except Exception as exc: + self.fail('Error when getting IoT Hub device {0}: {1}'.format(self.name, exc.message or str(exc))) + + def list_devices(self): + try: + url = '/devices' + return self._https_get(url, self.query_parameters, self.header_parameters) + except Exception as exc: + self.fail('Error when listing IoT Hub devices in {0}: {1}'.format(self.hub, exc.message or str(exc))) + + def _https_get(self, url, query_parameters, header_parameters): + request = self._mgmt_client.get(url, query_parameters) + response = self._mgmt_client.send(request=request, headers=header_parameters, content=None) + if response.status_code not in [200]: + raise CloudError(response) + return json.loads(response.text) + + +def main(): + AzureRMIoTDeviceFacts() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_iotdevicemodule.py b/lib/ansible/modules/cloud/azure/azure_rm_iotdevicemodule.py new file mode 100644 index 00000000000..55e1c55f4a3 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_iotdevicemodule.py @@ -0,0 +1,379 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 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_iotdevicemodule +version_added: "2.9" +short_description: Manage Azure IoT hub device module +description: + - Create, delete an Azure IoT hub device module. +options: + hub: + description: + - Name of IoT Hub. + type: str + required: true + hub_policy_name: + description: + - Policy name of the IoT Hub which will be used to query from IoT hub. + - This policy should have at least 'Registry Read' access. + type: str + required: true + hub_policy_key: + description: + - Key of the I(hub_policy_name). + type: str + required: true + name: + description: + - Name of the IoT hub device identity. + type: str + required: true + device: + description: + - Device name the module associate with. + required: true + type: str + state: + description: + - State of the IoT hub. Use C(present) to create or update an IoT hub device and C(absent) to delete an IoT hub device. + type: str + default: present + choices: + - absent + - present + auth_method: + description: + - The authorization type an entity is to be created with. + type: str + choices: + - sas + - certificate_authority + - self_signed + default: sas + primary_key: + description: + - Explicit self-signed certificate thumbprint to use for primary key. + - Explicit Shared Private Key to use for primary key. + type: str + aliases: + - primary_thumbprint + secondary_key: + description: + - Explicit self-signed certificate thumbprint to use for secondary key. + - Explicit Shared Private Key to use for secondary key. + type: str + aliases: + - secondary_thumbprint + twin_tags: + description: + - A section that the solution back end can read from and write to. + - Tags are not visible to device apps. + - "The tag can be nested dictionary, '.', '$', '#', ' ' is not allowed in the key." + - List is not supported. + type: dict + desired: + description: + - Used along with reported properties to synchronize device configuration or conditions. + - "The tag can be nested dictionary, '.', '$', '#', ' ' is not allowed in the key." + - List is not supported. + type: dict +extends_documentation_fragment: + - azure + - azure_tags + +author: + - Yuwei Zhou (@yuwzho) + +''' + +EXAMPLES = ''' +- name: Create simplest Azure IoT Hub device module + azure_rm_iotdevicemodule: + hub: myHub + name: Testing + device: mydevice + hub_policy_name: iothubowner + hub_policy_key: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + +- name: Create Azure IoT Edge device module + azure_rm_iotdevice: + hub: myHub + device: mydevice + name: Testing + hub_policy_name: iothubowner + hub_policy_key: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + edge_enabled: yes + +- name: Create Azure IoT Hub device module with module twin properties and tag + azure_rm_iotdevice: + hub: myHub + name: Testing + device: mydevice + hub_policy_name: iothubowner + hub_policy_key: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + twin_tags: + location: + country: US + city: Redmond + sensor: humidity + desired: + period: 100 +''' + +RETURN = ''' +module: + description: + - IoT Hub device. + returned: always + type: dict + sample: { + "authentication": { + "symmetricKey": { + "primaryKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "secondaryKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + }, + "type": "sas", + "x509Thumbprint": { + "primaryThumbprint": null, + "secondaryThumbprint": null + } + }, + "cloudToDeviceMessageCount": 0, + "connectionState": "Disconnected", + "connectionStateUpdatedTime": "0001-01-01T00:00:00", + "deviceId": "mydevice", + "etag": "ODM2NjI3ODg=", + "generationId": "636904759703045768", + "lastActivityTime": "0001-01-01T00:00:00", + "managedBy": null, + "moduleId": "Testing" + } +''' # NOQA + +import json +import copy +import re + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase, format_resource_id +from ansible.module_utils.common.dict_transformations import _snake_to_camel + +try: + from msrestazure.tools import parse_resource_id + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMIoTDeviceModule(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + name=dict(type='str', required=True), + hub_policy_name=dict(type='str', required=True), + hub_policy_key=dict(type='str', required=True), + hub=dict(type='str', required=True), + device=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + twin_tags=dict(type='dict'), + desired=dict(type='dict'), + auth_method=dict(type='str', choices=['self_signed', 'sas', 'certificate_authority'], default='sas'), + primary_key=dict(type='str', no_log=True, aliases=['primary_thumbprint']), + secondary_key=dict(type='str', no_log=True, aliases=['secondary_thumbprint']) + ) + + self.results = dict( + changed=False, + id=None + ) + + self.name = None + self.hub = None + self.device = None + self.hub_policy_key = None + self.hub_policy_name = None + self.state = None + self.twin_tags = None + self.desired = None + self.auth_method = None + self.primary_key = None + self.secondary_key = None + + required_if = [ + ['auth_method', 'self_signed', ['certificate_authority']] + ] + + self._base_url = None + self._mgmt_client = None + self.query_parameters = { + 'api-version': '2018-06-30' + } + self.header_parameters = { + 'Content-Type': 'application/json; charset=utf-8', + 'accept-language': 'en-US' + } + super(AzureRMIoTDeviceModule, self).__init__(self.module_arg_spec, supports_check_mode=True, required_if=required_if) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec.keys(): + setattr(self, key, kwargs[key]) + + self._base_url = '{0}.azure-devices.net'.format(self.hub) + config = { + 'base_url': self._base_url, + 'key': self.hub_policy_key, + 'policy': self.hub_policy_name + } + self._mgmt_client = self.get_data_svc_client(**config) + + changed = False + + module = self.get_module() + if self.state == 'present': + if not module: + changed = True + auth = {'type': _snake_to_camel(self.auth_method)} + if self.auth_method == 'self_signed': + auth['x509Thumbprint'] = { + 'primaryThumbprint': self.primary_key, + 'secondaryThumbprint': self.secondary_key + } + elif self.auth_method == 'sas': + auth['symmetricKey'] = { + 'primaryKey': self.primary_key, + 'secondaryKey': self.secondary_key + } + module = { + 'deviceId': self.device, + 'moduleId': self.name, + 'authentication': auth + } + if changed and not self.check_mode: + module = self.create_or_update_module(module) + twin = self.get_twin() + if not twin.get('tags'): + twin['tags'] = dict() + twin_change = False + if self.twin_tags and not self.is_equal(self.twin_tags, twin['tags']): + twin_change = True + if self.desired and not self.is_equal(self.desired, twin['properties']['desired']): + self.module.warn('desired') + twin_change = True + if twin_change and not self.check_mode: + twin = self.update_twin(twin) + changed = changed or twin_change + module['tags'] = twin.get('tags') or dict() + module['properties'] = twin['properties'] + elif module: + if not self.check_mode: + self.delete_module(module['etag']) + changed = True + module = None + self.results = module or dict() + self.results['changed'] = changed + return self.results + + def is_equal(self, updated, original): + changed = False + if not isinstance(updated, dict): + self.fail('The Property or Tag should be a dict') + for key in updated.keys(): + if re.search(r'[.|$|#|\s]', key): + self.fail("Property or Tag name has invalid characters: '.', '$', '#' or ' '. Got '{0}'".format(key)) + original_value = original.get(key) + updated_value = updated[key] + if isinstance(updated_value, dict): + if not isinstance(original_value, dict): + changed = True + original[key] = updated_value + elif not self.is_equal(updated_value, original_value): + changed = True + elif original_value != updated_value: + changed = True + original[key] = updated_value + return not changed + + def create_or_update_module(self, module): + try: + url = '/devices/{0}/modules/{1}'.format(self.device, self.name) + headers = copy.copy(self.header_parameters) + if module.get('etag'): + headers['If-Match'] = '"{0}"'.format(module['etag']) + request = self._mgmt_client.put(url, self.query_parameters) + response = self._mgmt_client.send(request=request, headers=headers, content=module) + if response.status_code not in [200, 201]: + raise CloudError(response) + return json.loads(response.text) + except Exception as exc: + self.fail('Error when creating or updating IoT Hub device {0}: {1}'.format(self.name, exc.message or str(exc))) + + def delete_module(self, etag): + try: + url = '/devices/{0}/modules/{1}'.format(self.device, self.name) + headers = copy.copy(self.header_parameters) + headers['If-Match'] = '"{0}"'.format(etag) + request = self._mgmt_client.delete(url, self.query_parameters) + response = self._mgmt_client.send(request=request, headers=headers) + if response.status_code not in [204]: + raise CloudError(response) + except Exception as exc: + self.fail('Error when deleting IoT Hub device {0}: {1}'.format(self.name, exc.message or str(exc))) + + def get_module(self): + try: + url = '/devices/{0}/modules/{1}'.format(self.device, self.name) + return self._https_get(url, self.query_parameters, self.header_parameters) + except Exception: + pass + return None + + def get_twin(self): + try: + url = '/twins/{0}/modules/{1}'.format(self.device, self.name) + return self._https_get(url, self.query_parameters, self.header_parameters) + except Exception as exc: + self.fail('Error when getting IoT Hub device {0} module twin {1}: {2}'.format(self.device, self.name, exc.message or str(exc))) + + def update_twin(self, twin): + try: + url = '/twins/{0}/modules/{1}'.format(self.device, self.name) + headers = copy.copy(self.header_parameters) + headers['If-Match'] = twin['etag'] + request = self._mgmt_client.patch(url, self.query_parameters) + response = self._mgmt_client.send(request=request, headers=headers, content=twin) + if response.status_code not in [200]: + raise CloudError(response) + return json.loads(response.text) + except Exception as exc: + self.fail('Error when creating or updating IoT Hub device {0} module twin {1}: {2}'.format(self.device, self.name, exc.message or str(exc))) + + def _https_get(self, url, query_parameters, header_parameters): + request = self._mgmt_client.get(url, query_parameters) + response = self._mgmt_client.send(request=request, headers=header_parameters, content=None) + if response.status_code not in [200]: + raise CloudError(response) + return json.loads(response.text) + + +def main(): + AzureRMIoTDeviceModule() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_iothub.py b/lib/ansible/modules/cloud/azure/azure_rm_iothub.py new file mode 100644 index 00000000000..142c0d1d4f0 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_iothub.py @@ -0,0 +1,896 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 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_iothub +version_added: "2.9" +short_description: Manage Azure IoT hub +description: + - Create, delete an Azure IoT hub. +options: + resource_group: + description: + - Name of resource group. + type: str + required: true + name: + description: + - Name of the IoT hub. + type: str + required: true + state: + description: + - State of the IoT hub. Use C(present) to create or update an IoT hub and C(absent) to delete an IoT hub. + type: str + default: present + choices: + - absent + - present + location: + description: + - Location of the IoT hub. + type: str + sku: + description: + - Pricing tier for Azure IoT Hub. + - Note that only one free IoT hub instance is allowed in each subscription. Exception will be thrown if free instances exceed one. + - Default is C(s1) when creation. + type: str + choices: + - b1 + - b2 + - b3 + - f1 + - s1 + - s2 + - s3 + unit: + description: + - Units in your IoT Hub. + - Default is C(1). + type: int + event_endpoint: + description: + - The Event Hub-compatible endpoint property. + type: dict + suboptions: + partition_count: + description: + - The number of partitions for receiving device-to-cloud messages in the Event Hub-compatible endpoint. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#device-to-cloud-messages)." + - Default is C(2). + type: int + retention_time_in_days: + description: + - The retention time for device-to-cloud messages in days. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#device-to-cloud-messages)." + - Default is C(1). + type: int + enable_file_upload_notifications: + description: + - File upload notifications are enabled if set to C(True). + type: bool + ip_filters: + description: + - Configure rules for rejecting or accepting traffic from specific IPv4 addresses. + type: list + suboptions: + name: + description: + - Name of the filter. + type: str + required: yes + ip_mask: + description: + - A string that contains the IP address range in CIDR notation for the rule. + type: str + required: yes + action: + description: + - The desired action for requests captured by this rule. + type: str + required: yes + choices: + - accept + - reject + routing_endpoints: + description: + - Custom endpoints. + type: list + suboptions: + name: + description: + - Name of the custom endpoint. + type: str + required: yes + resource_group: + description: + - Resource group of the endpoint. + - Default is the same as I(resource_group). + type: str + subscription: + description: + - Subscription id of the endpoint. + - Default is the same as I(subscription). + type: str + resource_type: + description: + - Resource type of the custom endpoint. + type: str + choices: + - eventhub + - queue + - storage + - topic + required: yes + connection_string: + description: + - Connection string of the custom endpoint. + - The connection string should have send priviledge. + type: str + required: yes + container: + description: + - Container name of the custom endpoint when I(resource_type=storage). + type: str + encoding: + description: + - Encoding of the message when I(resource_type=storage). + type: str + routes: + description: + - Route device-to-cloud messages to service-facing endpoints. + type: list + suboptions: + name: + description: + - Name of the route. + type: str + required: yes + source: + description: + - The origin of the data stream to be acted upon. + type: str + choices: + - device_messages + - twin_change_events + - device_lifecycle_events + - device_job_lifecycle_events + required: yes + enabled: + description: + - Whether to enable the route. + type: bool + required: yes + endpoint_name: + description: + - The name of the endpoint in I(routing_endpoints) where IoT Hub sends messages that match the query. + type: str + required: yes + condition: + description: + - "The query expression for the routing query that is run against the message application properties, + system properties, message body, device twin tags, and device twin properties to determine if it is a match for the endpoint." + - "For more information about constructing a query, + see U(https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-routing-query-syntax)" + type: str +extends_documentation_fragment: + - azure + - azure_tags + +author: + - Yuwei Zhou (@yuwzho) + +''' + +EXAMPLES = ''' +- name: Create a simplest IoT hub + azure_rm_iothub: + name: Testing + resource_group: myResourceGroup +- name: Create an IoT hub with route + azure_rm_iothub: + resource_group: myResourceGroup + name: Testing + routing_endpoints: + - connection_string: "Endpoint=sb://qux.servicebus.windows.net/;SharedAccessKeyName=quux;SharedAccessKey=****;EntityPath=myQueue" + name: foo + resource_type: queue + resource_group: myResourceGroup1 + routes: + - name: bar + source: device_messages + endpoint_name: foo + enabled: yes +''' + +RETURN = ''' +id: + description: + - Resource ID of the IoT hub. + sample: "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/myResourceGroup/providers/Microsoft.Devices/IotHubs/Testing" + returned: success + type: str +name: + description: + - Name of the IoT hub. + sample: Testing + returned: success + type: str +resource_group: + description: + - Resource group of the IoT hub. + sample: myResourceGroup. + returned: success + type: str +location: + description: + - Location of the IoT hub. + sample: eastus + returned: success + type: str +unit: + description: + - Units in the IoT Hub. + sample: 1 + returned: success + type: int +sku: + description: + - Pricing tier for Azure IoT Hub. + sample: f1 + returned: success + type: str +cloud_to_device: + description: + - Cloud to device message properties. + contains: + max_delivery_count: + description: + - The number of times the IoT hub attempts to deliver a message on the feedback queue. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#cloud-to-device-messages)." + type: int + returned: success + sample: 10 + ttl_as_iso8601: + description: + - The period of time for which a message is available to consume before it is expired by the IoT hub. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#cloud-to-device-messages)." + type: str + returned: success + sample: "1:00:00" + returned: success + type: complex +enable_file_upload_notifications: + description: + - Whether file upload notifications are enabled. + sample: True + returned: success + type: bool +event_endpoints: + description: + - Built-in endpoint where to deliver device message. + contains: + endpoint: + description: + - The Event Hub-compatible endpoint. + type: str + returned: success + sample: "sb://iothub-ns-testing-1478811-9bbc4a15f0.servicebus.windows.net/" + partition_count: + description: + - The number of partitions for receiving device-to-cloud messages in the Event Hub-compatible endpoint. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#device-to-cloud-messages)." + type: int + returned: success + sample: 2 + retention_time_in_days: + description: + - The retention time for device-to-cloud messages in days. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#device-to-cloud-messages)." + type: int + returned: success + sample: 1 + partition_ids: + description: + - List of the partition id for the event endpoint. + type: list + returned: success + sample: ["0", "1"] + returned: success + type: complex +host_name: + description: + - Host of the IoT hub. + sample: "testing.azure-devices.net" + returned: success + type: str +ip_filters: + description: + - Configure rules for rejecting or accepting traffic from specific IPv4 addresses. + contains: + name: + description: + - Name of the filter. + type: str + returned: success + sample: filter + ip_mask: + description: + - A string that contains the IP address range in CIDR notation for the rule. + type: str + returned: success + sample: 40.54.7.3 + action: + description: + - The desired action for requests captured by this rule. + type: str + returned: success + sample: Reject + returned: success + type: complex +routing_endpoints: + description: + - Custom endpoints. + contains: + event_hubs: + description: + - List of custom endpoints of event hubs. + type: complex + returned: success + contains: + name: + description: + - Name of the custom endpoint. + type: str + returned: success + sample: foo + resource_group: + description: + - Resource group of the endpoint. + type: str + returned: success + sample: bar + subscription: + description: + - Subscription id of the endpoint. + type: str + returned: success + sample: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + connection_string: + description: + - Connection string of the custom endpoint. + type: str + returned: success + sample: "Endpoint=sb://quux.servicebus.windows.net:5671/;SharedAccessKeyName=qux;SharedAccessKey=****;EntityPath=foo" + service_bus_queues: + description: + - List of custom endpoints of service bus queue. + type: complex + returned: always + contains: + name: + description: + - Name of the custom endpoint. + type: str + returned: success + sample: foo + resource_group: + description: + - Resource group of the endpoint. + type: str + returned: success + sample: bar + subscription: + description: + - Subscription ID of the endpoint. + type: str + returned: success + sample: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + connection_string: + description: + - Connection string of the custom endpoint. + type: str + returned: success + sample: "Endpoint=sb://quux.servicebus.windows.net:5671/;SharedAccessKeyName=qux;SharedAccessKey=****;EntityPath=foo" + service_bus_topics: + description: + - List of custom endpoints of service bus topic. + type: complex + returned: success + contains: + name: + description: + - Name of the custom endpoint. + type: str + returned: success + sample: foo + resource_group: + description: + - Resource group of the endpoint. + type: str + returned: success + sample: bar + subscription: + description: + - Subscription ID of the endpoint. + type: str + returned: success + sample: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + connection_string: + description: + - Connection string of the custom endpoint. + type: str + returned: success + sample: "Endpoint=sb://quux.servicebus.windows.net:5671/;SharedAccessKeyName=qux;SharedAccessKey=****;EntityPath=foo" + storage_containers: + description: + - List of custom endpoints of storage + type: complex + returned: success + contains: + name: + description: + - Name of the custom endpoint. + type: str + returned: success + sample: foo + resource_group: + description: + - Resource group of the endpoint. + type: str + returned: success + sample: bar + subscription: + description: + - Subscription ID of the endpoint. + type: str + returned: success + sample: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + connection_string: + description: + - Connection string of the custom endpoint. + type: str + returned: success + sample: "Endpoint=sb://quux.servicebus.windows.net:5671/;SharedAccessKeyName=qux;SharedAccessKey=****;EntityPath=foo" + returned: success + type: complex +routes: + description: + - Route device-to-cloud messages to service-facing endpoints. + type: complex + returned: success + contains: + name: + description: + - Name of the route. + type: str + returned: success + sample: route1 + source: + description: + - The origin of the data stream to be acted upon. + type: str + returned: success + sample: device_messages + enabled: + description: + - Whether to enable the route. + type: str + returned: success + sample: true + endpoint_name: + description: + - The name of the endpoint in C(routing_endpoints) where IoT Hub sends messages that match the query. + type: str + returned: success + sample: foo + condition: + description: + - "The query expression for the routing query that is run against the message application properties, + system properties, message body, device twin tags, and device twin properties to determine if it is a match for the endpoint." + - "For more information about constructing a query, + see I(https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-routing-query-syntax)" + type: bool + returned: success + sample: "true" +''' # NOQA + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase, format_resource_id +from ansible.module_utils.common.dict_transformations import _snake_to_camel, _camel_to_snake +import re + +try: + from msrestazure.tools import parse_resource_id + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +ip_filter_spec = dict( + name=dict(type='str', required=True), + ip_mask=dict(type='str', required=True), + action=dict(type='str', required=True, choices=['accept', 'reject']) +) + + +routing_endpoints_spec = dict( + connection_string=dict(type='str', required=True), + name=dict(type='str', required=True), + resource_group=dict(type='str'), + subscription=dict(type='str'), + resource_type=dict(type='str', required=True, choices=['eventhub', 'queue', 'storage', 'topic']), + container=dict(type='str'), + encoding=dict(type='str') +) + + +routing_endpoints_resource_type_mapping = { + 'eventhub': {'model': 'RoutingEventHubProperties', 'attribute': 'event_hubs'}, + 'queue': {'model': 'RoutingServiceBusQueueEndpointProperties', 'attribute': 'service_bus_queues'}, + 'topic': {'model': 'RoutingServiceBusTopicEndpointProperties', 'attribute': 'service_bus_topics'}, + 'storage': {'model': 'RoutingStorageContainerProperties', 'attribute': 'storage_containers'} +} + + +routes_spec = dict( + name=dict(type='str', required=True), + source=dict(type='str', required=True, choices=['device_messages', 'twin_change_events', 'device_lifecycle_events', 'device_job_lifecycle_events']), + enabled=dict(type='bool', required=True), + endpoint_name=dict(type='str', required=True), + condition=dict(type='str') +) + + +event_endpoint_spec = dict( + partition_count=dict(type='int'), + retention_time_in_days=dict(type='int') +) + + +class AzureRMIoTHub(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']), + location=dict(type='str'), + sku=dict(type='str', choices=['b1', 'b2', 'b3', 'f1', 's1', 's2', 's3']), + unit=dict(type='int'), + event_endpoint=dict(type='dict', options=event_endpoint_spec), + enable_file_upload_notifications=dict(type='bool'), + ip_filters=dict(type='list', elements='dict', options=ip_filter_spec), + routing_endpoints=dict(type='list', elements='dict', options=routing_endpoints_spec), + routes=dict(type='list', elements='dict', options=routes_spec) + ) + + self.results = dict( + changed=False, + id=None + ) + + self.resource_group = None + self.name = None + self.state = None + self.location = None + self.sku = None + self.unit = None + self.event_endpoint = None + self.tags = None + self.enable_file_upload_notifications = None + self.ip_filters = None + self.routing_endpoints = None + self.routes = None + + super(AzureRMIoTHub, self).__init__(self.module_arg_spec, supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()) + ['tags']: + setattr(self, key, kwargs[key]) + + changed = False + + if not self.location: + # Set default location + resource_group = self.get_resource_group(self.resource_group) + self.location = resource_group.location + self.sku = str.capitalize(self.sku) if self.sku else None + iothub = self.get_hub() + if self.state == 'present': + if not iothub: + changed = True + self.sku = self.sku or 'S1' + self.unit = self.unit or 1 + self.event_endpoint = self.event_endpoint or {} + self.event_endpoint['partition_count'] = self.event_endpoint.get('partition_count') or 2 + self.event_endpoint['retention_time_in_days'] = self.event_endpoint.get('retention_time_in_days') or 1 + event_hub_properties = dict() + event_hub_properties['events'] = self.IoThub_models.EventHubProperties(**self.event_endpoint) + iothub_property = self.IoThub_models.IotHubProperties(event_hub_endpoints=event_hub_properties) + if self.enable_file_upload_notifications: + iothub_property.enable_file_upload_notifications = self.enable_file_upload_notifications + if self.ip_filters: + iothub_property.ip_filter_rules = self.construct_ip_filters() + routing_endpoints = None + routes = None + if self.routing_endpoints: + routing_endpoints = self.construct_routing_endpoint(self.routing_endpoints) + if self.routes: + routes = [self.construct_route(x) for x in self.routes] + if routes or routing_endpoints: + routing_property = self.IoThub_models.RoutingProperties(endpoints=routing_endpoints, + routes=routes) + iothub_property.routing = routing_property + iothub = self.IoThub_models.IotHubDescription(location=self.location, + sku=self.IoThub_models.IotHubSkuInfo(name=self.sku, capacity=self.unit), + properties=iothub_property, + tags=self.tags) + if not self.check_mode: + iothub = self.create_or_update_hub(iothub) + else: + # compare sku + original_sku = iothub.sku + if self.sku and self.sku != original_sku.name: + self.log('SKU changed') + iothub.sku.name = self.sku + changed = True + if self.unit and self.unit != original_sku.capacity: + self.log('Unit count changed') + iothub.sku.capacity = self.unit + changed = True + # compare event hub property + event_hub = iothub.properties.event_hub_endpoints or dict() + if self.event_endpoint: + item = self.event_endpoint + original_item = event_hub.get('events') + if not original_item: + changed = True + event_hub['events'] = self.IoThub_models.EventHubProperties(partition_count=item.get('partition_count') or 2, + retention_time_in_days=item.get('retention_time_in_days') or 1) + elif item.get('partition_count') and original_item.partition_count != item['partition_count']: + changed = True + original_item.partition_count = item['partition_count'] + elif item.get('retention_time_in_days') and original_item.retention_time_in_days != item['retention_time_in_days']: + changed = True + original_item.retention_time_in_days = item['retention_time_in_days'] + # compare endpoint + original_endpoints = iothub.properties.routing.endpoints + endpoint_changed = False + if self.routing_endpoints: + # find the total length + total_length = 0 + for item in routing_endpoints_resource_type_mapping.values(): + attribute = item['attribute'] + array = getattr(original_endpoints, attribute) + total_length += len(array or []) + if total_length != len(self.routing_endpoints): + endpoint_changed = True + else: # If already changed, no need to compare any more + for item in self.routing_endpoints: + if not self.lookup_endpoint(item, original_endpoints): + endpoint_changed = True + break + if endpoint_changed: + iothub.properties.routing.endpoints = self.construct_routing_endpoint(self.routing_endpoints) + changed = True + # compare routes + original_routes = iothub.properties.routing.routes + routes_changed = False + if self.routes: + if len(self.routes) != len(original_routes or []): + routes_changed = True + else: + for item in self.routes: + if not self.lookup_route(item, original_routes): + routes_changed = True + break + if routes_changed: + changed = True + iothub.properties.routing.routes = [self.construct_route(x) for x in self.routes] + # compare IP filter + ip_filter_changed = False + original_ip_filter = iothub.properties.ip_filter_rules + if self.ip_filters: + if len(self.ip_filters) != len(original_ip_filter or []): + ip_filter_changed = True + else: + for item in self.ip_filters: + if not self.lookup_ip_filter(item, original_ip_filter): + ip_filter_changed = True + break + if ip_filter_changed: + changed = True + iothub.properties.ip_filter_rules = self.construct_ip_filters() + + # compare tags + tag_changed, updated_tags = self.update_tags(iothub.tags) + iothub.tags = updated_tags + if changed and not self.check_mode: + iothub = self.create_or_update_hub(iothub) + # only tags changed + if not changed and tag_changed: + changed = True + if not self.check_mode: + iothub = self.update_instance_tags(updated_tags) + self.results = self.to_dict(iothub) + elif iothub: + changed = True + if not self.check_mode: + self.delete_hub() + self.results['changed'] = changed + return self.results + + def lookup_ip_filter(self, target, ip_filters): + if not ip_filters or len(ip_filters) == 0: + return False + for item in ip_filters: + if item.filter_name == target['name']: + if item.ip_mask != target['ip_mask']: + return False + if item.action.lower() != target['action']: + return False + return True + return False + + def lookup_route(self, target, routes): + if not routes or len(routes) == 0: + return False + for item in routes: + if item.name == target['name']: + if target['source'] != _camel_to_snake(item.source): + return False + if target['enabled'] != item.is_enabled: + return False + if target['endpoint_name'] != item.endpoint_names[0]: + return False + if target.get('condition') and target['condition'] != item.condition: + return False + return True + return False + + def lookup_endpoint(self, target, routing_endpoints): + resource_type = target['resource_type'] + attribute = routing_endpoints_resource_type_mapping[resource_type]['attribute'] + endpoints = getattr(routing_endpoints, attribute) + if not endpoints or len(endpoints) == 0: + return False + for item in endpoints: + if item.name == target['name']: + if target.get('resource_group') and target['resource_group'] != (item.resource_group or self.resource_group): + return False + if target.get('subscription_id') and target['subscription_id'] != (item.subscription_id or self.subscription_id): + return False + connection_string_regex = item.connection_string.replace('****', '.*') + connection_string_regex = re.sub(r':\d+/;', '/;', connection_string_regex) + if not re.search(connection_string_regex, target['connection_string']): + return False + if resource_type == 'storage': + if target.get('container') and item.container_name != target['container']: + return False + if target.get('encoding') and item.encoding != target['encoding']: + return False + return True + return False + + def construct_ip_filters(self): + return [self.IoThub_models.IpFilterRule(filter_name=x['name'], + action=self.IoThub_models.IpFilterActionType[x['action']], + ip_mask=x['ip_mask']) for x in self.ip_filters] + + def construct_routing_endpoint(self, routing_endpoints): + if not routing_endpoints or len(routing_endpoints) == 0: + return None + result = self.IoThub_models.RoutingEndpoints() + for endpoint in routing_endpoints: + resource_type_property = routing_endpoints_resource_type_mapping.get(endpoint['resource_type']) + resource_type = getattr(self.IoThub_models, resource_type_property['model']) + array = getattr(result, resource_type_property['attribute']) or [] + array.append(resource_type(**endpoint)) + setattr(result, resource_type_property['attribute'], array) + return result + + def construct_route(self, route): + if not route: + return None + return self.IoThub_models.RouteProperties(name=route['name'], + source=_snake_to_camel(snake=route['source'], capitalize_first=True), + is_enabled=route['enabled'], + endpoint_names=[route['endpoint_name']], + condition=route.get('condition')) + + def get_hub(self): + try: + return self.IoThub_client.iot_hub_resource.get(self.resource_group, self.name) + except Exception: + pass + return None + + def create_or_update_hub(self, hub): + try: + poller = self.IoThub_client.iot_hub_resource.create_or_update(self.resource_group, self.name, hub, if_match=hub.etag) + return self.get_poller_result(poller) + except Exception as exc: + self.fail('Error creating or updating IoT Hub {0}: {1}'.format(self.name, exc.message or str(exc))) + + def update_instance_tags(self, tags): + try: + poller = self.IoThub_client.iot_hub_resource.update(self.resource_group, self.name, tags=tags) + return self.get_poller_result(poller) + except Exception as exc: + self.fail('Error updating IoT Hub {0}\'s tag: {1}'.format(self.name, exc.message or str(exc))) + + def delete_hub(self): + try: + self.IoThub_client.iot_hub_resource.delete(self.resource_group, self.name) + return True + except Exception as exc: + self.fail('Error deleting IoT Hub {0}: {1}'.format(self.name, exc.message or str(exc))) + return False + + def route_to_dict(self, route): + return dict( + name=route.name, + source=_camel_to_snake(route.source), + endpoint_name=route.endpoint_names[0], + enabled=route.is_enabled, + condition=route.condition + ) + + def instance_dict_to_dict(self, instance_dict): + result = dict() + if not instance_dict: + return result + for key in instance_dict.keys(): + result[key] = instance_dict[key].as_dict() + return result + + def to_dict(self, hub): + result = dict() + properties = hub.properties + result['id'] = hub.id + result['name'] = hub.name + result['resource_group'] = self.resource_group + result['location'] = hub.location + result['tags'] = hub.tags + result['unit'] = hub.sku.capacity + result['sku'] = hub.sku.name.lower() + result['cloud_to_device'] = dict( + max_delivery_count=properties.cloud_to_device.feedback.max_delivery_count, + ttl_as_iso8601=str(properties.cloud_to_device.feedback.ttl_as_iso8601) + ) if properties.cloud_to_device else dict() + result['enable_file_upload_notifications'] = properties.enable_file_upload_notifications + result['event_endpoint'] = properties.event_hub_endpoints.get('events').as_dict() if properties.event_hub_endpoints.get('events') else None + result['host_name'] = properties.host_name + result['ip_filters'] = [x.as_dict() for x in properties.ip_filter_rules] + if properties.routing: + result['routing_endpoints'] = properties.routing.endpoints.as_dict() + result['routes'] = [self.route_to_dict(x) for x in properties.routing.routes] + result['fallback_route'] = self.route_to_dict(properties.routing.fallback_route) + result['status'] = properties.state + result['storage_endpoints'] = self.instance_dict_to_dict(properties.storage_endpoints) + return result + + +def main(): + AzureRMIoTHub() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_iothub_info.py b/lib/ansible/modules/cloud/azure/azure_rm_iothub_info.py new file mode 100644 index 00000000000..150522e4238 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_iothub_info.py @@ -0,0 +1,618 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 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_iothub_info + +version_added: "2.9" + +short_description: Get IoT Hub facts + +description: + - Get facts for a specific IoT Hub or all IoT Hubs. + +options: + name: + description: + - Limit results to a specific resource group. + type: str + resource_group: + description: + - The resource group to search for the desired IoT Hub. + type: str + tags: + description: + - Limit results by providing a list of tags. Format tags as 'key' or 'key:value'. + type: list + show_stats: + description: + - Show the statistics for IoT Hub. + - Note this will have network overhead for each IoT Hub. + type: bool + show_quota_metrics: + description: + - Get the quota metrics for an IoT hub. + - Note this will have network overhead for each IoT Hub. + type: bool + show_endpoint_health: + description: + - Get the health for routing endpoints. + - Note this will have network overhead for each IoT Hub. + type: bool + test_route_message: + description: + - Test routes message. It will be used to test all routes. + type: str + list_consumer_groups: + description: + - List the consumer group of the built-in event hub. + type: bool + list_keys: + description: + - List the keys of IoT Hub. + - Note this will have network overhead for each IoT Hub. + type: bool +extends_documentation_fragment: + - azure + +author: + - Yuwei Zhou (@yuwzho) +''' + +EXAMPLES = ''' + - name: Get facts for one IoT Hub + azure_rm_iothub_info: + name: Testing + resource_group: myResourceGroup + + - name: Get facts for all IoT Hubs + azure_rm_iothub_info: + + - name: Get facts for all IoT Hubs in a specific resource group + azure_rm_iothub_info: + resource_group: myResourceGroup + + - name: Get facts by tags + azure_rm_iothub_info: + tags: + - testing +''' + +RETURN = ''' +azure_iothubs: + description: + - List of IoT Hub dicts. + returned: always + type: complex + contains: + id: + description: + - Resource ID of the IoT hub. + type: str + returned: always + sample: "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/myResourceGroup/providers/Microsoft.Devices/IotHubs/Testing" + name: + description: + - Name of the IoT hub. + type: str + returned: always + sample: Testing + resource_group: + description: + - Resource group of the IoT hub. + type: str + returned: always + sample: myResourceGroup. + location: + description: + - Location of the IoT hub. + type: str + returned: always + sample: eastus + unit: + description: + - Units in the IoT Hub. + type: int + returned: always + sample: 1 + sku: + description: + - Pricing tier for Azure IoT Hub. + type: str + returned: always + sample: f1 + cloud_to_device: + description: + - Cloud to device message properties. + type: complex + returned: always + contains: + max_delivery_count: + description: + - The number of times the IoT hub attempts to deliver a message on the feedback queue. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#cloud-to-device-messages)." + type: int + returned: always + sample: 10 + ttl_as_iso8601: + description: + - The period of time for which a message is available to consume before it is expired by the IoT hub. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#cloud-to-device-messages)." + type: str + returned: always + sample: "1:00:00" + enable_file_upload_notifications: + description: + - Whether file upload notifications are enabled. + type: str + returned: always + sample: True + event_endpoints: + description: + - Built-in endpoint where to deliver device message. + type: complex + returned: always + contains: + endpoint: + description: + - The Event Hub-compatible endpoint. + type: str + returned: always + sample: "sb://iothub-ns-testing-1478811-9bbc4a15f0.servicebus.windows.net/" + partition_count: + description: + - The number of partitions for receiving device-to-cloud messages in the Event Hub-compatible endpoint. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#device-to-cloud-messages)." + type: int + returned: always + sample: 2 + retention_time_in_days: + description: + - The retention time for device-to-cloud messages in days. + - "See U(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging#device-to-cloud-messages)." + type: int + returned: always + sample: 1 + partition_ids: + description: + - List of the partition id for the event endpoint. + type: list + returned: always + sample: ["0", "1"] + host_name: + description: + - Host of the IoT hub. + type: str + returned: always + sample: "testing.azure-devices.net" + ip_filters: + description: + - Configure rules for rejecting or accepting traffic from specific IPv4 addresses. + type: complex + returned: always + contains: + name: + description: + - Name of the filter. + type: str + returned: always + sample: filter + ip_mask: + description: + - A string that contains the IP address range in CIDR notation for the rule. + type: str + returned: always + sample: 40.54.7.3 + action: + description: + - The desired action for requests captured by this rule. + type: str + returned: always + sample: Reject + routing_endpoints: + description: + - Custom endpoints. + type: complex + returned: always + contains: + event_hubs: + description: + - List of custom endpoints of event hubs. + type: complex + returned: always + contains: + name: + description: + - Name of the custom endpoint. + type: str + returned: always + sample: foo + resource_group: + description: + - Resource group of the endpoint. + type: str + returned: always + sample: bar + subscription: + description: + - Subscription ID of the endpoint. + type: str + returned: always + sample: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + connection_string: + description: + - Connection string of the custom endpoint. + type: str + returned: always + sample: "Endpoint=sb://quux.servicebus.windows.net:5671/;SharedAccessKeyName=qux;SharedAccessKey=****;EntityPath=foo" + service_bus_queues: + description: + - List of custom endpoints of service bus queue. + type: complex + returned: always + contains: + name: + description: + - Name of the custom endpoint. + type: str + returned: always + sample: foo + resource_group: + description: + - Resource group of the endpoint. + type: str + returned: always + sample: bar + subscription: + description: + - Subscription ID of the endpoint. + type: str + returned: always + sample: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + connection_string: + description: + - Connection string of the custom endpoint. + type: str + returned: always + sample: "Endpoint=sb://quux.servicebus.windows.net:5671/;SharedAccessKeyName=qux;SharedAccessKey=****;EntityPath=foo" + service_bus_topics: + description: + - List of custom endpoints of service bus topic. + type: complex + returned: always + contains: + name: + description: + - Name of the custom endpoint. + type: str + returned: always + sample: foo + resource_group: + description: + - Resource group of the endpoint. + type: str + returned: always + sample: bar + subscription: + description: + - Subscription ID of the endpoint. + type: str + returned: always + sample: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + connection_string: + description: + - Connection string of the custom endpoint. + type: str + returned: always + sample: "Endpoint=sb://quux.servicebus.windows.net:5671/;SharedAccessKeyName=qux;SharedAccessKey=****;EntityPath=foo" + storage_containers: + description: + - List of custom endpoints of storage. + type: complex + returned: always + contains: + name: + description: + - Name of the custom endpoint. + type: str + returned: always + sample: foo + resource_group: + description: + - Resource group of the endpoint. + type: str + returned: always + sample: bar + subscription: + description: + - Subscription ID of the endpoint. + type: str + returned: always + sample: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + connection_string: + description: + - Connection string of the custom endpoint. + type: str + returned: always + sample: "Endpoint=sb://quux.servicebus.windows.net:5671/;SharedAccessKeyName=qux;SharedAccessKey=****;EntityPath=foo" + routes: + description: + - Route device-to-cloud messages to service-facing endpoints. + type: complex + returned: always + contains: + name: + description: + - Name of the route. + type: str + returned: always + sample: route1 + source: + description: + - The origin of the data stream to be acted upon. + type: str + returned: always + sample: device_messages + enabled: + description: + - Whether to enable the route. + type: bool + returned: always + sample: true + endpoint_name: + description: + - The name of the endpoint in I(routing_endpoints) where IoT Hub sends messages that match the query. + type: str + returned: always + sample: foo + condition: + description: + - "The query expression for the routing query that is run against the message application properties, + system properties, message body, device twin tags, and device twin properties to determine if it is a match for the endpoint." + - "For more information about constructing a query, + see U(https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-routing-query-syntax)" + type: bool + returned: always + sample: "true" + tags: + description: + - Limit results by providing a list of tags. Format tags as 'key' or 'key:value'. + type: dict + returned: always + sample: { 'key1': 'value1' } +''' + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase +from ansible.module_utils.common.dict_transformations import _camel_to_snake + +try: + from msrestazure.azure_exceptions import CloudError + from msrestazure.tools import parse_resource_id + from azure.common import AzureHttpError +except Exception: + # handled in azure_rm_common + pass + + +class AzureRMIoTHubFacts(AzureRMModuleBase): + """Utility class to get IoT Hub facts""" + + def __init__(self): + + self.module_args = dict( + name=dict(type='str'), + resource_group=dict(type='str'), + tags=dict(type='list'), + show_stats=dict(type='bool'), + show_quota_metrics=dict(type='bool'), + show_endpoint_health=dict(type='bool'), + list_keys=dict(type='bool'), + test_route_message=dict(type='str'), + list_consumer_groups=dict(type='bool') + ) + + self.results = dict( + changed=False, + azure_iothubs=[] + ) + + self.name = None + self.resource_group = None + self.tags = None + self.show_stats = None + self.show_quota_metrics = None + self.show_endpoint_health = None + self.list_keys = None + self.test_route_message = None + self.list_consumer_groups = None + + super(AzureRMIoTHubFacts, self).__init__( + derived_arg_spec=self.module_args, + supports_tags=False, + facts_module=True + ) + + def exec_module(self, **kwargs): + + for key in self.module_args: + setattr(self, key, kwargs[key]) + + response = [] + if self.name: + response = self.get_item() + elif self.resource_group: + response = self.list_by_resource_group() + else: + response = self.list_all() + self.results['iothubs'] = [self.to_dict(x) for x in response if self.has_tags(x.tags, self.tags)] + return self.results + + def get_item(self): + """Get a single IoT Hub""" + + self.log('Get properties for {0}'.format(self.name)) + + item = None + + try: + item = self.IoThub_client.iot_hub_resource.get(self.resource_group, self.name) + return [item] + except Exception as exc: + self.fail('Error when getting IoT Hub {0}: {1}'.format(self.name, exc.message or str(exc))) + + def list_all(self): + """Get all IoT Hubs""" + + self.log('List all IoT Hubs') + + try: + return self.IoThub_client.iot_hub_resource.list_by_subscription() + except Exception as exc: + self.fail('Failed to list all IoT Hubs - {0}'.format(str(exc))) + + def list_by_resource_group(self): + try: + return self.IoThub_client.iot_hub_resource.list(self.resource_group) + except Exception as exc: + self.fail('Failed to list IoT Hub in resource group {0} - {1}'.format(self.resource_group, exc.message or str(exc))) + + def show_hub_stats(self, resource_group, name): + try: + return self.IoThub_client.iot_hub_resource.get_stats(resource_group, name).as_dict() + except Exception as exc: + self.fail('Failed to getting statistics for IoT Hub {0}/{1}: {2}'.format(resource_group, name, str(exc))) + + def show_hub_quota_metrics(self, resource_group, name): + result = [] + try: + resp = self.IoThub_client.iot_hub_resource.get_quota_metrics(resource_group, name) + while True: + result.append(resp.next().as_dict()) + except StopIteration: + pass + except Exception as exc: + self.fail('Failed to getting quota metrics for IoT Hub {0}/{1}: {2}'.format(resource_group, name, str(exc))) + return result + + def show_hub_endpoint_health(self, resource_group, name): + result = [] + try: + resp = self.IoThub_client.iot_hub_resource.get_endpoint_health(resource_group, name) + while True: + result.append(resp.next().as_dict()) + except StopIteration: + pass + except Exception as exc: + self.fail('Failed to getting health for IoT Hub {0}/{1} routing endpoint: {2}'.format(resource_group, name, str(exc))) + return result + + def test_all_routes(self, resource_group, name): + try: + return self.IoThub_client.iot_hub_resource.test_all_routes(self.test_route_message, resource_group, name).routes.as_dict() + except Exception as exc: + self.fail('Failed to getting statistics for IoT Hub {0}/{1}: {2}'.format(resource_group, name, str(exc))) + + def list_hub_keys(self, resource_group, name): + result = [] + try: + resp = self.IoThub_client.iot_hub_resource.list_keys(resource_group, name) + while True: + result.append(resp.next().as_dict()) + except StopIteration: + pass + except Exception as exc: + self.fail('Failed to getting health for IoT Hub {0}/{1} routing endpoint: {2}'.format(resource_group, name, str(exc))) + return result + + def list_event_hub_consumer_groups(self, resource_group, name, event_hub_endpoint='events'): + result = [] + try: + resp = self.IoThub_client.iot_hub_resource.list_event_hub_consumer_groups(resource_group, name, event_hub_endpoint) + while True: + cg = resp.next() + result.append(dict( + id=cg.id, + name=cg.name + )) + except StopIteration: + pass + except Exception as exc: + self.fail('Failed to listing consumer group for IoT Hub {0}/{1} routing endpoint: {2}'.format(resource_group, name, str(exc))) + return result + + def route_to_dict(self, route): + return dict( + name=route.name, + source=_camel_to_snake(route.source), + endpoint_name=route.endpoint_names[0], + enabled=route.is_enabled, + condition=route.condition + ) + + def instance_dict_to_dict(self, instance_dict): + result = dict() + for key in instance_dict.keys(): + result[key] = instance_dict[key].as_dict() + return result + + def to_dict(self, hub): + result = dict() + properties = hub.properties + result['id'] = hub.id + result['name'] = hub.name + result['resource_group'] = parse_resource_id(hub.id).get('resource_group') + result['location'] = hub.location + result['tags'] = hub.tags + result['unit'] = hub.sku.capacity + result['sku'] = hub.sku.name.lower() + result['cloud_to_device'] = dict( + max_delivery_count=properties.cloud_to_device.feedback.max_delivery_count, + ttl_as_iso8601=str(properties.cloud_to_device.feedback.ttl_as_iso8601) + ) + result['enable_file_upload_notifications'] = properties.enable_file_upload_notifications + result['event_hub_endpoints'] = self.instance_dict_to_dict(properties.event_hub_endpoints) + result['host_name'] = properties.host_name + result['ip_filters'] = [x.as_dict() for x in properties.ip_filter_rules] + result['routing_endpoints'] = properties.routing.endpoints.as_dict() + result['routes'] = [self.route_to_dict(x) for x in properties.routing.routes] + result['fallback_route'] = self.route_to_dict(properties.routing.fallback_route) + result['status'] = properties.state + result['storage_endpoints'] = self.instance_dict_to_dict(properties.storage_endpoints) + + # network overhead part + if self.show_stats: + result['statistics'] = self.show_hub_stats(result['resource_group'], hub.name) + if self.show_quota_metrics: + result['quota_metrics'] = self.show_hub_quota_metrics(result['resource_group'], hub.name) + if self.show_endpoint_health: + result['endpoint_health'] = self.show_hub_endpoint_health(result['resource_group'], hub.name) + if self.list_keys: + result['keys'] = self.list_hub_keys(result['resource_group'], hub.name) + if self.test_route_message: + result['test_route_result'] = self.test_all_routes(result['resource_group'], hub.name) + if self.list_consumer_groups: + result['consumer_groups'] = self.list_event_hub_consumer_groups(result['resource_group'], hub.name) + return result + + +def main(): + """Main module execution code path""" + + AzureRMIoTHubFacts() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_iothubconsumergroup.py b/lib/ansible/modules/cloud/azure/azure_rm_iothubconsumergroup.py new file mode 100644 index 00000000000..75b5ad77c9e --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_iothubconsumergroup.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 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_iothubconsumergroup +version_added: "2.9" +short_description: Manage Azure IoT hub +description: + - Create, delete an Azure IoT hub. +options: + resource_group: + description: + - Name of resource group. + type: str + required: true + hub: + description: + - Name of the IoT hub. + type: str + required: true + state: + description: + - State of the IoT hub. Use C(present) to create or update an IoT hub and C(absent) to delete an IoT hub. + type: str + default: present + choices: + - absent + - present + event_hub: + description: + - Event hub endpoint name. + type: str + default: events + name: + description: + - Name of the consumer group. + type: str +extends_documentation_fragment: + - azure + - azure_tags + +author: + - Yuwei Zhou (@yuwzho) + +''' + +EXAMPLES = ''' +- name: Create an IoT hub consumer group + azure_rm_iothubconsumergroup: + name: test + resource_group: myResourceGroup + hub: Testing +''' + +RETURN = ''' +id: + description: + - Resource ID of the consumer group. + returned: success + type: str + sample: "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/myResourceGroup + /providers/Microsoft.Devices/IotHubs/Testing/events/ConsumerGroups/%24Default" +name: + description: + - Name of the consumer group. + sample: Testing + returned: success + type: str +''' # NOQA + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase, format_resource_id +from ansible.module_utils.common.dict_transformations import _snake_to_camel, _camel_to_snake +import re + +try: + from msrestazure.tools import parse_resource_id + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMIoTHubConsumerGroup(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']), + hub=dict(type='str', required=True), + event_hub=dict(type='str', default='events') + ) + + self.results = dict( + changed=False, + id=None + ) + + self.resource_group = None + self.name = None + self.state = None + self.hub = None + self.event_hub = None + + super(AzureRMIoTHubConsumerGroup, self).__init__(self.module_arg_spec, supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec.keys(): + setattr(self, key, kwargs[key]) + + changed = False + cg = self.get_cg() + if not cg and self.state == 'present': + changed = True + if not self.check_mode: + cg = self.create_cg() + elif cg and self.state == 'absent': + changed = True + cg = None + if not self.check_mode: + self.delete_cg() + self.results = dict( + id=cg.id, + name=cg.name + ) if cg else dict() + self.results['changed'] = changed + return self.results + + def get_cg(self): + try: + return self.IoThub_client.iot_hub_resource.get_event_hub_consumer_group(self.resource_group, self.hub, self.event_hub, self.name) + except Exception: + pass + return None + + def create_cg(self): + try: + return self.IoThub_client.iot_hub_resource.create_event_hub_consumer_group(self.resource_group, self.hub, self.event_hub, self.name) + except Exception as exc: + self.fail('Error when creating the consumer group {0} for IoT Hub {1} event hub {2}: {3}'.format(self.name, self.hub, self.event_hub, str(exc))) + + def delete_cg(self): + try: + return self.IoThub_client.iot_hub_resource.delete_event_hub_consumer_group(self.resource_group, self.hub, self.event_hub, self.name) + except Exception as exc: + self.fail('Error when deleting the consumer group {0} for IoT Hub {1} event hub {2}: {3}'.format(self.name, self.hub, self.event_hub, str(exc))) + + +def main(): + AzureRMIoTHubConsumerGroup() + + +if __name__ == '__main__': + main() diff --git a/packaging/requirements/requirements-azure.txt b/packaging/requirements/requirements-azure.txt index 3b7fa69b937..37f112f0258 100644 --- a/packaging/requirements/requirements-azure.txt +++ b/packaging/requirements/requirements-azure.txt @@ -36,3 +36,4 @@ azure-mgmt-hdinsight==0.1.0 azure-mgmt-devtestlabs==3.0.0 azure-mgmt-loganalytics==0.2.0 azure-mgmt-automation==0.1.1 +azure-mgmt-iothub==0.7.0 diff --git a/test/integration/targets/azure_rm_iothub/aliases b/test/integration/targets/azure_rm_iothub/aliases new file mode 100644 index 00000000000..aa77c071a84 --- /dev/null +++ b/test/integration/targets/azure_rm_iothub/aliases @@ -0,0 +1,3 @@ +cloud/azure +shippable/azure/group2 +destructive diff --git a/test/integration/targets/azure_rm_iothub/meta/main.yml b/test/integration/targets/azure_rm_iothub/meta/main.yml new file mode 100644 index 00000000000..95e1952f989 --- /dev/null +++ b/test/integration/targets/azure_rm_iothub/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/test/integration/targets/azure_rm_iothub/tasks/main.yml b/test/integration/targets/azure_rm_iothub/tasks/main.yml new file mode 100644 index 00000000000..6055ea1f85a --- /dev/null +++ b/test/integration/targets/azure_rm_iothub/tasks/main.yml @@ -0,0 +1,172 @@ +- set_fact: + rpfx: "{{ resource_group | hash('md5') | truncate(8, True, '') }}" + +- name: Create IoT Hub (check mode) + azure_rm_iothub: + name: "hub{{ rpfx }}" + resource_group: "{{ resource_group }}" + ip_filters: + - name: filter1 + action: reject + ip_mask: 40.60.80.10 + check_mode: yes + register: iothub + +- assert: + that: + - iothub.changed + +- name: Query IoT Hub + azure_rm_iothub_info: + name: "hub{{ rpfx }}" + resource_group: "{{ resource_group }}" + register: iothub + ignore_errors: yes + +- name: Create IoT Hub + azure_rm_iothub: + name: "hub{{ rpfx }}" + resource_group: "{{ resource_group }}" + ip_filters: + - name: filter1 + action: reject + ip_mask: 40.60.80.10 + register: iothub + +- assert: + that: + - iothub.changed + +- name: Create IoT Hub (idempontent) + azure_rm_iothub: + name: "hub{{ rpfx }}" + resource_group: "{{ resource_group }}" + ip_filters: + - name: filter1 + action: reject + ip_mask: 40.60.80.10 + register: iothub + +- assert: + that: + - not iothub.changed + +- name: Query IoT Hub + azure_rm_iothub_info: + name: "hub{{ rpfx }}" + resource_group: "{{ resource_group }}" + list_keys: yes + register: iothub + +- assert: + that: + - iothub.iothubs | length == 1 + +- set_fact: + registry_write_name: "{{ item.key_name }}" + registry_write_key: "{{ item.primary_key }}" + with_items: "{{ iothub.iothubs[0]['keys'] }}" + when: item.rights == 'RegistryWrite, ServiceConnect, DeviceConnect' + +- name: Create devices + azure_rm_iotdevice: + hub: "hub{{ rpfx }}" + hub_policy_name: "{{ registry_write_name }}" + hub_policy_key: "{{ registry_write_key }}" + name: "mydevice{{ item }}" + twin_tags: + location: + country: US + city: Redmond + sensor: humidity + with_items: + - 1 + - 2 + +- name: Query devices + azure_rm_iotdevice_info: + hub: "hub{{ rpfx }}" + hub_policy_name: "{{ registry_write_name }}" + hub_policy_key: "{{ registry_write_key }}" + register: devices + +- assert: + that: + - devices.iot_devices | length == 2 + +- name: Query devices + azure_rm_iotdevice_info: + hub: "hub{{ rpfx }}" + name: "mydevice1" + hub_policy_name: "{{ registry_write_name }}" + hub_policy_key: "{{ registry_write_key }}" + register: devices + +- assert: + that: + - devices.iot_devices | length == 1 + - devices.iot_devices[0].deviceId == 'mydevice1' + +- name: Query devices twin + azure_rm_iotdevice_info: + hub: "hub{{ rpfx }}" + query: "SELECT * FROM devices WHERE tags.location.country = 'US'" + hub_policy_name: "{{ registry_write_name }}" + hub_policy_key: "{{ registry_write_key }}" + register: devices + +- assert: + that: + - devices.iot_devices | length == 2 + +- name: Update devices + azure_rm_iotdevice: + hub: "hub{{ rpfx }}" + hub_policy_name: "{{ registry_write_name }}" + hub_policy_key: "{{ registry_write_key }}" + name: "mydevice{{ item }}" + edge_enabled: yes + twin_tags: + location: + country: China + city: Shanghai + sensor: humidity + with_items: + - 1 + - 3 + +- name: Query devices twin + azure_rm_iotdevice_info: + hub: "hub{{ rpfx }}" + query: "SELECT * FROM devices WHERE tags.location.country = 'US'" + hub_policy_name: "{{ registry_write_name }}" + hub_policy_key: "{{ registry_write_key }}" + register: devices + +- assert: + that: + - devices.iot_devices | length == 1 + - devices.iot_devices[0].deviceId == 'mydevice2' + +- name: Delete IoT Hub (check mode) + azure_rm_iothub: + name: "hub{{ rpfx }}" + resource_group: "{{ resource_group }}" + state: absent + check_mode: yes + register: iothub + +- assert: + that: + - iothub.changed + +- name: Delete IoT Hub + azure_rm_iothub: + name: "hub{{ rpfx }}" + resource_group: "{{ resource_group }}" + state: absent + register: iothub + +- assert: + that: + - iothub.changed diff --git a/test/lib/ansible_test/_data/requirements/integration.cloud.azure.txt b/test/lib/ansible_test/_data/requirements/integration.cloud.azure.txt index 3b7fa69b937..37f112f0258 100644 --- a/test/lib/ansible_test/_data/requirements/integration.cloud.azure.txt +++ b/test/lib/ansible_test/_data/requirements/integration.cloud.azure.txt @@ -36,3 +36,4 @@ azure-mgmt-hdinsight==0.1.0 azure-mgmt-devtestlabs==3.0.0 azure-mgmt-loganalytics==0.2.0 azure-mgmt-automation==0.1.1 +azure-mgmt-iothub==0.7.0