From 562a08eba665a633d7894d4905f964a3c22cc8a8 Mon Sep 17 00:00:00 2001 From: prbans Date: Mon, 13 Apr 2020 15:44:24 -0700 Subject: [PATCH 01/11] Adding ability to get DCM given device_id --- azext_iot/commands.py | 7 ++ azext_iot/operations/central.py | 11 +++ azext_iot/providers/__init__.py | 9 +++ .../providers/central_device_provider.py | 73 +++++++++++++++++++ azext_iot/service/__init__.py | 5 ++ azext_iot/service/auth.py | 27 +++++++ azext_iot/service/central/__init__.py | 10 +++ azext_iot/service/central/_utility.py | 16 ++++ azext_iot/service/central/device.py | 52 +++++++++++++ azext_iot/service/central/device_template.py | 52 +++++++++++++ 10 files changed, 262 insertions(+) create mode 100644 azext_iot/providers/__init__.py create mode 100644 azext_iot/providers/central_device_provider.py create mode 100644 azext_iot/service/__init__.py create mode 100644 azext_iot/service/auth.py create mode 100644 azext_iot/service/central/__init__.py create mode 100644 azext_iot/service/central/_utility.py create mode 100644 azext_iot/service/central/device.py create mode 100644 azext_iot/service/central/device_template.py diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 7fdfc0d88..c4ba95710 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -184,6 +184,13 @@ def load_command_table(self, _): "validate-messages", "iot_central_validate_messages", is_preview=True ) + with self.command_group( + "iot central app capability-model", command_type=iotcentral_ops + ) as cmd_group: + cmd_group.command( + "get", "iot_central_device_capability_model_get", is_preview=True + ) + with self.command_group( "iot central device-twin", command_type=iotcentral_ops ) as cmd_group: diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 14e740261..04c6c6e87 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -4,12 +4,15 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import six + from knack.util import CLIError from azext_iot._factory import _bind_sdk from azext_iot.common._azure import get_iot_hub_token_from_central_app_id from azext_iot.common.shared import SdkType from azext_iot.common.utility import unpack_msrest_error, init_monitoring from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication +from azext_iot.providers import CentralDeviceProvider def find_between(s, start, end): @@ -30,6 +33,14 @@ def iot_central_device_show( raise CLIError(unpack_msrest_error(e)) +def iot_central_device_capability_model_get( + cmd, device_id, app_name, central_api_uri="api.azureiotcentral.com" +): + provider = CentralDeviceProvider() + device_template = provider.get_device_template(cmd, device_id, app_name) + six.print_(device_template) + + def iot_central_validate_messages( cmd, app_id, diff --git a/azext_iot/providers/__init__.py b/azext_iot/providers/__init__.py new file mode 100644 index 000000000..4f4610751 --- /dev/null +++ b/azext_iot/providers/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from .central_device_provider import CentralDeviceProvider + +__all__ = ["CentralDeviceProvider"] diff --git a/azext_iot/providers/central_device_provider.py b/azext_iot/providers/central_device_provider.py new file mode 100644 index 000000000..79fe24a43 --- /dev/null +++ b/azext_iot/providers/central_device_provider.py @@ -0,0 +1,73 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.service.central import CentralDeviceClient, CentralDeviceTemplateClient + + +class CentralDeviceProvider: + _device_templates = {} + _devices = {} + + def __init__(self): + self._device_template_client = CentralDeviceTemplateClient() + self._device_client = CentralDeviceClient() + pass + + def get_device_template( + self, + cmd, + device_id, + app_name, + token=None, + central_dns_suffix="azureiotcentral.com", + ): + device = self.get_device(cmd, device_id, app_name, token, central_dns_suffix) + device_template_urn = device["instanceOf"] + + if not device_template_urn: + raise ValueError( + "No device template urn found for device '{}'".format(device_id) + ) + + if ( + device_template_urn not in self._device_templates + or not self._device_templates.get(device_template_urn) + ): + self._device_templates[ + device_template_urn + ] = self._device_template_client.get_device_template( + cmd, device_template_urn, app_name, token, central_dns_suffix + ) + + device_template = self._device_templates.get(device_template_urn) + if not device_template: + raise UnboundLocalError( + "No device template for device with id: '{}'.".format(device_id) + ) + + return device_template + + def get_device( + self, + cmd, + device_id, + app_name, + token=None, + central_dns_suffix="azureiotcentral.com", + ): + if not device_id: + raise ValueError("Device id must be specified.") + + if device_id not in self._devices or not self._devices.get(device_id): + self._devices[device_id] = self._device_client.get_device( + cmd, device_id, app_name, token, central_dns_suffix + ) + + device = self._devices.get(device_id) + if not device: + raise UnboundLocalError("No device found with id: '{}'.".format(device_id)) + + return device diff --git a/azext_iot/service/__init__.py b/azext_iot/service/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/service/__init__.py @@ -0,0 +1,5 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/azext_iot/service/auth.py b/azext_iot/service/auth.py new file mode 100644 index 000000000..a952b81f9 --- /dev/null +++ b/azext_iot/service/auth.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core._profile import Profile + + +def get_aad_token(cmd, resource=None): + """ + get AAD token to access to a specified resource + :param resource: Azure resource endpoints. Default to Azure Resource Manager + Use 'az cloud show' command for other Azure resources + """ + resource = resource or cmd.cli_ctx.cloud.endpoints.active_directory_resource_id + profile = Profile(cli_ctx=cmd.cli_ctx) + creds, subscription, tenant = profile.get_raw_token( + subscription=None, resource=resource + ) + return { + "tokenType": creds[0], + "accessToken": creds[1], + "expiresOn": creds[2].get("expiresOn", "N/A"), + "subscription": subscription, + "tenant": tenant, + } diff --git a/azext_iot/service/central/__init__.py b/azext_iot/service/central/__init__.py new file mode 100644 index 000000000..54e980092 --- /dev/null +++ b/azext_iot/service/central/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from .device import CentralDeviceClient +from .device_template import CentralDeviceTemplateClient + +__all__ = ["CentralDeviceClient", "CentralDeviceTemplateClient"] diff --git a/azext_iot/service/central/_utility.py b/azext_iot/service/central/_utility.py new file mode 100644 index 000000000..94bd2eecb --- /dev/null +++ b/azext_iot/service/central/_utility.py @@ -0,0 +1,16 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# Nothing in this file should be used outside of service/central + +from azext_iot.service import auth + + +def get_token(token, cmd): + if not token: + token = auth.get_aad_token(cmd, resource="https://apps.azureiotcentral.com") + return "Bearer {}".format(token["accessToken"]) + + return token diff --git a/azext_iot/service/central/device.py b/azext_iot/service/central/device.py new file mode 100644 index 000000000..de6aa14bc --- /dev/null +++ b/azext_iot/service/central/device.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devices + +import requests + +from ._utility import get_token + + +class CentralDeviceClient: + def get_device( + self, + cmd, + device_id: str, + app_name: str, + token: str, + central_dns_suffix="azureiotcentral.com", + ) -> str: + """ + Get device info given a device id + + Args: + cmd: command passed into az + device_id: unique case-sensitive device id, + app_name: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ + + if not token: + token = get_token(token, cmd) + + url = "https://{}.{}/api/preview/devices/{}".format( + app_name, central_dns_suffix, device_id + ) + headers = {"Authorization": token} + + response = requests.get(url, headers=headers) + + body = response.json() + + if "error" in body: + raise Exception(body["error"]) + + return body diff --git a/azext_iot/service/central/device_template.py b/azext_iot/service/central/device_template.py new file mode 100644 index 000000000..9a284ae53 --- /dev/null +++ b/azext_iot/service/central/device_template.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devices + +import requests + +from ._utility import get_token + + +class CentralDeviceTemplateClient: + def get_device_template( + self, + cmd, + device_template_urn: str, + app_name: str, + token: str, + central_dns_suffix="azureiotcentral.com", + ) -> str: + """ + Get device template given a device id + + Args: + cmd: command passed into az + device_template_urn: case sensitive device template urn, + app_name: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ + + if not token: + token = get_token(token, cmd) + + url = "https://{}.{}/api/preview/deviceTemplates/{}".format( + app_name, central_dns_suffix, device_template_urn + ) + headers = {"Authorization": token} + + response = requests.get(url, headers=headers) + + body = response.json() + + if "error" in body: + raise Exception(body["error"]) + + return body From f91a8dbeaed6afe389da7b383719cbf4d29149c6 Mon Sep 17 00:00:00 2001 From: prbans Date: Mon, 13 Apr 2020 16:30:39 -0700 Subject: [PATCH 02/11] Moved some things around --- azext_iot/operations/central.py | 2 +- azext_iot/providers/__init__.py | 4 +- azext_iot/providers/central/__init__.py | 9 ++++ .../device_provider.py} | 8 ++- azext_iot/service/central/device.py | 52 ------------------- azext_iot/service/central/device_template.py | 52 ------------------- azext_iot/{service => services}/__init__.py | 0 azext_iot/{service => services}/auth.py | 0 .../{service => services}/central/__init__.py | 6 +-- .../{service => services}/central/_utility.py | 2 +- azext_iot/services/central/device.py | 50 ++++++++++++++++++ azext_iot/services/central/device_template.py | 50 ++++++++++++++++++ 12 files changed, 119 insertions(+), 116 deletions(-) create mode 100644 azext_iot/providers/central/__init__.py rename azext_iot/providers/{central_device_provider.py => central/device_provider.py} (86%) delete mode 100644 azext_iot/service/central/device.py delete mode 100644 azext_iot/service/central/device_template.py rename azext_iot/{service => services}/__init__.py (100%) rename azext_iot/{service => services}/auth.py (100%) rename azext_iot/{service => services}/central/__init__.py (69%) rename azext_iot/{service => services}/central/_utility.py (94%) create mode 100644 azext_iot/services/central/device.py create mode 100644 azext_iot/services/central/device_template.py diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 04c6c6e87..9eceedf74 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -12,7 +12,7 @@ from azext_iot.common.shared import SdkType from azext_iot.common.utility import unpack_msrest_error, init_monitoring from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication -from azext_iot.providers import CentralDeviceProvider +from azext_iot.providers.central import CentralDeviceProvider def find_between(s, start, end): diff --git a/azext_iot/providers/__init__.py b/azext_iot/providers/__init__.py index 4f4610751..248e4ce22 100644 --- a/azext_iot/providers/__init__.py +++ b/azext_iot/providers/__init__.py @@ -4,6 +4,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from .central_device_provider import CentralDeviceProvider +from . import central -__all__ = ["CentralDeviceProvider"] +__all__ = ["central"] diff --git a/azext_iot/providers/central/__init__.py b/azext_iot/providers/central/__init__.py new file mode 100644 index 000000000..21fd79ac1 --- /dev/null +++ b/azext_iot/providers/central/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from .device_provider import CentralDeviceProvider + +__all__ = ["CentralDeviceProvider"] diff --git a/azext_iot/providers/central_device_provider.py b/azext_iot/providers/central/device_provider.py similarity index 86% rename from azext_iot/providers/central_device_provider.py rename to azext_iot/providers/central/device_provider.py index 79fe24a43..d5a770db9 100644 --- a/azext_iot/providers/central_device_provider.py +++ b/azext_iot/providers/central/device_provider.py @@ -4,7 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_iot.service.central import CentralDeviceClient, CentralDeviceTemplateClient +from azext_iot.services import central class CentralDeviceProvider: @@ -12,8 +12,6 @@ class CentralDeviceProvider: _devices = {} def __init__(self): - self._device_template_client = CentralDeviceTemplateClient() - self._device_client = CentralDeviceClient() pass def get_device_template( @@ -38,7 +36,7 @@ def get_device_template( ): self._device_templates[ device_template_urn - ] = self._device_template_client.get_device_template( + ] = central.device_template.get_device_template( cmd, device_template_urn, app_name, token, central_dns_suffix ) @@ -62,7 +60,7 @@ def get_device( raise ValueError("Device id must be specified.") if device_id not in self._devices or not self._devices.get(device_id): - self._devices[device_id] = self._device_client.get_device( + self._devices[device_id] = central.device.get_device( cmd, device_id, app_name, token, central_dns_suffix ) diff --git a/azext_iot/service/central/device.py b/azext_iot/service/central/device.py deleted file mode 100644 index de6aa14bc..000000000 --- a/azext_iot/service/central/device.py +++ /dev/null @@ -1,52 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- -# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devices - -import requests - -from ._utility import get_token - - -class CentralDeviceClient: - def get_device( - self, - cmd, - device_id: str, - app_name: str, - token: str, - central_dns_suffix="azureiotcentral.com", - ) -> str: - """ - Get device info given a device id - - Args: - cmd: command passed into az - device_id: unique case-sensitive device id, - app_name: name of app (used for forming request URL) - token: (OPTIONAL) authorization token to fetch device details from IoTC. - MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') - central_dns_suffix: {centralDnsSuffixInPath} as found in docs - - Returns: - device: dict - """ - - if not token: - token = get_token(token, cmd) - - url = "https://{}.{}/api/preview/devices/{}".format( - app_name, central_dns_suffix, device_id - ) - headers = {"Authorization": token} - - response = requests.get(url, headers=headers) - - body = response.json() - - if "error" in body: - raise Exception(body["error"]) - - return body diff --git a/azext_iot/service/central/device_template.py b/azext_iot/service/central/device_template.py deleted file mode 100644 index 9a284ae53..000000000 --- a/azext_iot/service/central/device_template.py +++ /dev/null @@ -1,52 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- -# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devices - -import requests - -from ._utility import get_token - - -class CentralDeviceTemplateClient: - def get_device_template( - self, - cmd, - device_template_urn: str, - app_name: str, - token: str, - central_dns_suffix="azureiotcentral.com", - ) -> str: - """ - Get device template given a device id - - Args: - cmd: command passed into az - device_template_urn: case sensitive device template urn, - app_name: name of app (used for forming request URL) - token: (OPTIONAL) authorization token to fetch device details from IoTC. - MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') - central_dns_suffix: {centralDnsSuffixInPath} as found in docs - - Returns: - device: dict - """ - - if not token: - token = get_token(token, cmd) - - url = "https://{}.{}/api/preview/deviceTemplates/{}".format( - app_name, central_dns_suffix, device_template_urn - ) - headers = {"Authorization": token} - - response = requests.get(url, headers=headers) - - body = response.json() - - if "error" in body: - raise Exception(body["error"]) - - return body diff --git a/azext_iot/service/__init__.py b/azext_iot/services/__init__.py similarity index 100% rename from azext_iot/service/__init__.py rename to azext_iot/services/__init__.py diff --git a/azext_iot/service/auth.py b/azext_iot/services/auth.py similarity index 100% rename from azext_iot/service/auth.py rename to azext_iot/services/auth.py diff --git a/azext_iot/service/central/__init__.py b/azext_iot/services/central/__init__.py similarity index 69% rename from azext_iot/service/central/__init__.py rename to azext_iot/services/central/__init__.py index 54e980092..6e13f4bad 100644 --- a/azext_iot/service/central/__init__.py +++ b/azext_iot/services/central/__init__.py @@ -4,7 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from .device import CentralDeviceClient -from .device_template import CentralDeviceTemplateClient +from . import device, device_template -__all__ = ["CentralDeviceClient", "CentralDeviceTemplateClient"] + +__all__ = ["device", "device_template"] diff --git a/azext_iot/service/central/_utility.py b/azext_iot/services/central/_utility.py similarity index 94% rename from azext_iot/service/central/_utility.py rename to azext_iot/services/central/_utility.py index 94bd2eecb..357fd35dd 100644 --- a/azext_iot/service/central/_utility.py +++ b/azext_iot/services/central/_utility.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------------------------- # Nothing in this file should be used outside of service/central -from azext_iot.service import auth +from azext_iot.services import auth def get_token(token, cmd): diff --git a/azext_iot/services/central/device.py b/azext_iot/services/central/device.py new file mode 100644 index 000000000..9f30c1ce0 --- /dev/null +++ b/azext_iot/services/central/device.py @@ -0,0 +1,50 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devices + +import requests + +from ._utility import get_token + + +def get_device( + cmd, + device_id: str, + app_name: str, + token: str, + central_dns_suffix="azureiotcentral.com", +) -> str: + """ + Get device info given a device id + + Args: + cmd: command passed into az + device_id: unique case-sensitive device id, + app_name: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ + + if not token: + token = get_token(token, cmd) + + url = "https://{}.{}/api/preview/devices/{}".format( + app_name, central_dns_suffix, device_id + ) + headers = {"Authorization": token} + + response = requests.get(url, headers=headers) + + body = response.json() + + if "error" in body: + raise Exception(body["error"]) + + return body diff --git a/azext_iot/services/central/device_template.py b/azext_iot/services/central/device_template.py new file mode 100644 index 000000000..20c7b5db2 --- /dev/null +++ b/azext_iot/services/central/device_template.py @@ -0,0 +1,50 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devices + +import requests + +from ._utility import get_token + + +def get_device_template( + cmd, + device_template_urn: str, + app_name: str, + token: str, + central_dns_suffix="azureiotcentral.com", +) -> str: + """ + Get device template given a device id + + Args: + cmd: command passed into az + device_template_urn: case sensitive device template urn, + app_name: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ + + if not token: + token = get_token(token, cmd) + + url = "https://{}.{}/api/preview/deviceTemplates/{}".format( + app_name, central_dns_suffix, device_template_urn + ) + headers = {"Authorization": token} + + response = requests.get(url, headers=headers) + + body = response.json() + + if "error" in body: + raise Exception(body["error"]) + + return body From b83b756c80a1a5dde055f63c524a193a6dd6a4af Mon Sep 17 00:00:00 2001 From: prbans Date: Mon, 13 Apr 2020 16:37:28 -0700 Subject: [PATCH 03/11] Moved to using app-id --- azext_iot/operations/central.py | 4 ++-- azext_iot/providers/central/device_provider.py | 10 +++++----- azext_iot/services/central/device.py | 6 +++--- azext_iot/services/central/device_template.py | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 9eceedf74..50e795561 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -34,10 +34,10 @@ def iot_central_device_show( def iot_central_device_capability_model_get( - cmd, device_id, app_name, central_api_uri="api.azureiotcentral.com" + cmd, device_id, app_id, central_api_uri="api.azureiotcentral.com" ): provider = CentralDeviceProvider() - device_template = provider.get_device_template(cmd, device_id, app_name) + device_template = provider.get_device_template(cmd, device_id, app_id) six.print_(device_template) diff --git a/azext_iot/providers/central/device_provider.py b/azext_iot/providers/central/device_provider.py index d5a770db9..aac7e4f5c 100644 --- a/azext_iot/providers/central/device_provider.py +++ b/azext_iot/providers/central/device_provider.py @@ -18,11 +18,11 @@ def get_device_template( self, cmd, device_id, - app_name, + app_id, token=None, central_dns_suffix="azureiotcentral.com", ): - device = self.get_device(cmd, device_id, app_name, token, central_dns_suffix) + device = self.get_device(cmd, device_id, app_id, token, central_dns_suffix) device_template_urn = device["instanceOf"] if not device_template_urn: @@ -37,7 +37,7 @@ def get_device_template( self._device_templates[ device_template_urn ] = central.device_template.get_device_template( - cmd, device_template_urn, app_name, token, central_dns_suffix + cmd, device_template_urn, app_id, token, central_dns_suffix ) device_template = self._device_templates.get(device_template_urn) @@ -52,7 +52,7 @@ def get_device( self, cmd, device_id, - app_name, + app_id, token=None, central_dns_suffix="azureiotcentral.com", ): @@ -61,7 +61,7 @@ def get_device( if device_id not in self._devices or not self._devices.get(device_id): self._devices[device_id] = central.device.get_device( - cmd, device_id, app_name, token, central_dns_suffix + cmd, device_id, app_id, token, central_dns_suffix ) device = self._devices.get(device_id) diff --git a/azext_iot/services/central/device.py b/azext_iot/services/central/device.py index 9f30c1ce0..85fc7c983 100644 --- a/azext_iot/services/central/device.py +++ b/azext_iot/services/central/device.py @@ -13,7 +13,7 @@ def get_device( cmd, device_id: str, - app_name: str, + app_id: str, token: str, central_dns_suffix="azureiotcentral.com", ) -> str: @@ -23,7 +23,7 @@ def get_device( Args: cmd: command passed into az device_id: unique case-sensitive device id, - app_name: name of app (used for forming request URL) + app_id: name of app (used for forming request URL) token: (OPTIONAL) authorization token to fetch device details from IoTC. MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') central_dns_suffix: {centralDnsSuffixInPath} as found in docs @@ -36,7 +36,7 @@ def get_device( token = get_token(token, cmd) url = "https://{}.{}/api/preview/devices/{}".format( - app_name, central_dns_suffix, device_id + app_id, central_dns_suffix, device_id ) headers = {"Authorization": token} diff --git a/azext_iot/services/central/device_template.py b/azext_iot/services/central/device_template.py index 20c7b5db2..fea6425bb 100644 --- a/azext_iot/services/central/device_template.py +++ b/azext_iot/services/central/device_template.py @@ -13,7 +13,7 @@ def get_device_template( cmd, device_template_urn: str, - app_name: str, + app_id: str, token: str, central_dns_suffix="azureiotcentral.com", ) -> str: @@ -23,7 +23,7 @@ def get_device_template( Args: cmd: command passed into az device_template_urn: case sensitive device template urn, - app_name: name of app (used for forming request URL) + app_id: name of app (used for forming request URL) token: (OPTIONAL) authorization token to fetch device details from IoTC. MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') central_dns_suffix: {centralDnsSuffixInPath} as found in docs @@ -36,7 +36,7 @@ def get_device_template( token = get_token(token, cmd) url = "https://{}.{}/api/preview/deviceTemplates/{}".format( - app_name, central_dns_suffix, device_template_urn + app_id, central_dns_suffix, device_template_urn ) headers = {"Authorization": token} From 9da2858b82d63ef2242e363f043646bb6a49f78e Mon Sep 17 00:00:00 2001 From: prbans Date: Tue, 14 Apr 2020 10:29:52 -0700 Subject: [PATCH 04/11] output is now cleaner --- azext_iot/operations/central.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 50e795561..ca3b8f0b5 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -4,6 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import json import six from knack.util import CLIError @@ -38,7 +39,8 @@ def iot_central_device_capability_model_get( ): provider = CentralDeviceProvider() device_template = provider.get_device_template(cmd, device_id, app_id) - six.print_(device_template) + output = json.dumps(device_template, indent=4) + six.print_(output) def iot_central_validate_messages( From fbe057eed9c988735dc0633fa1e7767f7b9e932f Mon Sep 17 00:00:00 2001 From: prbans Date: Tue, 14 Apr 2020 13:24:32 -0700 Subject: [PATCH 05/11] Validate field names match DCM --- azext_iot/{services => central}/__init__.py | 0 .../{providers/central => central/providers}/__init__.py | 0 .../central => central/providers}/device_provider.py | 6 +++--- .../{services/central => central/services}/__init__.py | 0 .../{services/central => central/services}/_utility.py | 2 +- .../{services/central => central/services}/device.py | 0 .../central => central/services}/device_template.py | 0 azext_iot/{services => common}/auth.py | 0 azext_iot/operations/central.py | 2 +- azext_iot/providers/__init__.py | 9 --------- 10 files changed, 5 insertions(+), 14 deletions(-) rename azext_iot/{services => central}/__init__.py (100%) rename azext_iot/{providers/central => central/providers}/__init__.py (100%) rename azext_iot/{providers/central => central/providers}/device_provider.py (91%) rename azext_iot/{services/central => central/services}/__init__.py (100%) rename azext_iot/{services/central => central/services}/_utility.py (94%) rename azext_iot/{services/central => central/services}/device.py (100%) rename azext_iot/{services/central => central/services}/device_template.py (100%) rename azext_iot/{services => common}/auth.py (100%) delete mode 100644 azext_iot/providers/__init__.py diff --git a/azext_iot/services/__init__.py b/azext_iot/central/__init__.py similarity index 100% rename from azext_iot/services/__init__.py rename to azext_iot/central/__init__.py diff --git a/azext_iot/providers/central/__init__.py b/azext_iot/central/providers/__init__.py similarity index 100% rename from azext_iot/providers/central/__init__.py rename to azext_iot/central/providers/__init__.py diff --git a/azext_iot/providers/central/device_provider.py b/azext_iot/central/providers/device_provider.py similarity index 91% rename from azext_iot/providers/central/device_provider.py rename to azext_iot/central/providers/device_provider.py index aac7e4f5c..4882bca4f 100644 --- a/azext_iot/providers/central/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -4,7 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_iot.services import central +from azext_iot.central import services as central_services class CentralDeviceProvider: @@ -36,7 +36,7 @@ def get_device_template( ): self._device_templates[ device_template_urn - ] = central.device_template.get_device_template( + ] = central_services.device_template.get_device_template( cmd, device_template_urn, app_id, token, central_dns_suffix ) @@ -60,7 +60,7 @@ def get_device( raise ValueError("Device id must be specified.") if device_id not in self._devices or not self._devices.get(device_id): - self._devices[device_id] = central.device.get_device( + self._devices[device_id] = central_services.device.get_device( cmd, device_id, app_id, token, central_dns_suffix ) diff --git a/azext_iot/services/central/__init__.py b/azext_iot/central/services/__init__.py similarity index 100% rename from azext_iot/services/central/__init__.py rename to azext_iot/central/services/__init__.py diff --git a/azext_iot/services/central/_utility.py b/azext_iot/central/services/_utility.py similarity index 94% rename from azext_iot/services/central/_utility.py rename to azext_iot/central/services/_utility.py index 357fd35dd..2afa7e84a 100644 --- a/azext_iot/services/central/_utility.py +++ b/azext_iot/central/services/_utility.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------------------------- # Nothing in this file should be used outside of service/central -from azext_iot.services import auth +from azext_iot.common import auth def get_token(token, cmd): diff --git a/azext_iot/services/central/device.py b/azext_iot/central/services/device.py similarity index 100% rename from azext_iot/services/central/device.py rename to azext_iot/central/services/device.py diff --git a/azext_iot/services/central/device_template.py b/azext_iot/central/services/device_template.py similarity index 100% rename from azext_iot/services/central/device_template.py rename to azext_iot/central/services/device_template.py diff --git a/azext_iot/services/auth.py b/azext_iot/common/auth.py similarity index 100% rename from azext_iot/services/auth.py rename to azext_iot/common/auth.py diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index ca3b8f0b5..ae0febea2 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -13,7 +13,7 @@ from azext_iot.common.shared import SdkType from azext_iot.common.utility import unpack_msrest_error, init_monitoring from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication -from azext_iot.providers.central import CentralDeviceProvider +from azext_iot.central.providers import CentralDeviceProvider def find_between(s, start, end): diff --git a/azext_iot/providers/__init__.py b/azext_iot/providers/__init__.py deleted file mode 100644 index 248e4ce22..000000000 --- a/azext_iot/providers/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from . import central - -__all__ = ["central"] From 644b9f303b172bc17f51c88dd98ae45f51744f2b Mon Sep 17 00:00:00 2001 From: prbans Date: Tue, 14 Apr 2020 15:12:49 -0700 Subject: [PATCH 06/11] Validates telemetry name --- .../central/providers/device_provider.py | 43 +++++---- azext_iot/operations/central.py | 57 ++++++------ azext_iot/operations/events3/_events.py | 15 +++- azext_iot/operations/events3/_parser.py | 88 ++++++++++++++++--- 4 files changed, 146 insertions(+), 57 deletions(-) diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 4882bca4f..7922587ed 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -10,19 +10,31 @@ class CentralDeviceProvider: _device_templates = {} _devices = {} + _cmd = "" + _app_id = "" + _token = "" - def __init__(self): + def __init__(self, cmd, app_id, token=None): + """ + Provider for device/device_template APIs + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + Useful in scenarios where user doesn't own the app + therefore AAD token won't work, but a SAS token generated by owner will + """ + self._cmd = cmd + self._app_id = app_id + self._token = token pass def get_device_template( - self, - cmd, - device_id, - app_id, - token=None, - central_dns_suffix="azureiotcentral.com", + self, device_id, central_dns_suffix="azureiotcentral.com", ): - device = self.get_device(cmd, device_id, app_id, token, central_dns_suffix) + device = self.get_device(device_id, central_dns_suffix) device_template_urn = device["instanceOf"] if not device_template_urn: @@ -37,7 +49,11 @@ def get_device_template( self._device_templates[ device_template_urn ] = central_services.device_template.get_device_template( - cmd, device_template_urn, app_id, token, central_dns_suffix + self._cmd, + device_template_urn, + self._app_id, + self._token, + central_dns_suffix, ) device_template = self._device_templates.get(device_template_urn) @@ -49,19 +65,14 @@ def get_device_template( return device_template def get_device( - self, - cmd, - device_id, - app_id, - token=None, - central_dns_suffix="azureiotcentral.com", + self, device_id, central_dns_suffix="azureiotcentral.com", ): if not device_id: raise ValueError("Device id must be specified.") if device_id not in self._devices or not self._devices.get(device_id): self._devices[device_id] = central_services.device.get_device( - cmd, device_id, app_id, token, central_dns_suffix + self._cmd, device_id, self._app_id, self._token, central_dns_suffix ) device = self._devices.get(device_id) diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index ae0febea2..c95d89e68 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -37,8 +37,8 @@ def iot_central_device_show( def iot_central_device_capability_model_get( cmd, device_id, app_id, central_api_uri="api.azureiotcentral.com" ): - provider = CentralDeviceProvider() - device_template = provider.get_device_template(cmd, device_id, app_id) + provider = CentralDeviceProvider(cmd, app_id) + device_template = provider.get_device_template(device_id) output = json.dumps(device_template, indent=4) six.print_(output) @@ -56,19 +56,21 @@ def iot_central_validate_messages( yes=False, central_api_uri="api.azureiotcentral.com", ): + provider = CentralDeviceProvider(cmd, app_id) _events3_runner( - cmd, - app_id, - device_id, - True, - simulate_errors, - consumer_group, - timeout, - enqueued_time, - repair, - properties, - yes, - central_api_uri, + cmd=cmd, + app_id=app_id, + device_id=device_id, + validate_messages=True, + simulate_errors=simulate_errors, + consumer_group=consumer_group, + timeout=timeout, + enqueued_time=enqueued_time, + repair=repair, + properties=properties, + yes=yes, + central_api_uri=central_api_uri, + central_device_provider=provider, ) @@ -85,18 +87,19 @@ def iot_central_monitor_events( central_api_uri="api.azureiotcentral.com", ): _events3_runner( - cmd, - app_id, - device_id, - False, - False, - consumer_group, - timeout, - enqueued_time, - repair, - properties, - yes, - central_api_uri, + cmd=cmd, + app_id=app_id, + device_id=device_id, + validate_messages=False, + simulate_errors=False, + consumer_group=consumer_group, + timeout=timeout, + enqueued_time=enqueued_time, + repair=repair, + properties=properties, + yes=yes, + central_api_uri=central_api_uri, + central_device_provider=None, ) @@ -113,6 +116,7 @@ def _events3_runner( properties, yes, central_api_uri, + central_device_provider, ): (enqueued_time, properties, timeout, output) = init_monitoring( cmd, timeout, properties, enqueued_time, repair, yes @@ -134,4 +138,5 @@ def _events3_runner( output=output, validate_messages=validate_messages, simulate_errors=simulate_errors, + central_device_provider=central_device_provider, ) diff --git a/azext_iot/operations/events3/_events.py b/azext_iot/operations/events3/_events.py index 278ed22ec..0df0a6d12 100644 --- a/azext_iot/operations/events3/_events.py +++ b/azext_iot/operations/events3/_events.py @@ -38,6 +38,7 @@ def executor( pnp_context=None, validate_messages=False, simulate_errors=False, + central_device_provider=None, ): coroutines = [] @@ -56,6 +57,7 @@ def executor( pnp_context, validate_messages, simulate_errors, + central_device_provider, ) ) @@ -113,6 +115,7 @@ async def initiate_event_monitor( pnp_context=None, validate_messages=False, simulate_errors=False, + central_device_provider=None, ): def _get_conn_props(): properties = {} @@ -155,6 +158,7 @@ def _get_conn_props(): pnp_context=pnp_context, validate_messages=validate_messages, simulate_errors=simulate_errors, + central_device_provider=central_device_provider, ) ) return await asyncio.gather(*coroutines, return_exceptions=True) @@ -178,6 +182,7 @@ async def monitor_events( pnp_context=None, validate_messages=False, simulate_errors=False, + central_device_provider=None, ): source = uamqp.address.Source( "amqps://{}/{}/ConsumerGroups/{}/Partitions/{}".format( @@ -214,6 +219,7 @@ async def monitor_events( output, validate_messages, simulate_errors, + central_device_provider, ) except asyncio.CancelledError: @@ -368,6 +374,7 @@ def _output_msg_kpi( output, validate_messages, simulate_errors, + central_device_provider, ): parser = Event3Parser() origin_device_id = parser.parse_device_id(msg) @@ -376,7 +383,13 @@ def _output_msg_kpi( return parsed_msg = parser.parse_message( - msg, pnp_context, interface_name, properties, content_type, simulate_errors + msg, + pnp_context, + interface_name, + properties, + content_type, + simulate_errors, + central_device_provider, ) if output.lower() == "json": diff --git a/azext_iot/operations/events3/_parser.py b/azext_iot/operations/events3/_parser.py index 345d5bc10..b9b932a29 100644 --- a/azext_iot/operations/events3/_parser.py +++ b/azext_iot/operations/events3/_parser.py @@ -11,6 +11,7 @@ from knack.log import get_logger from uamqp.message import Message from azext_iot.common.utility import parse_entity, unicode_binary_map +from azext_iot.central.providers import CentralDeviceProvider SUPPORTED_ENCODINGS = ["utf-8"] DEVICE_ID_IDENTIFIER = b"iothub-connection-device-id" @@ -36,22 +37,26 @@ def parse_message( properties: dict, content_type_hint: str, simulate_errors: bool, + central_device_provider: CentralDeviceProvider, ) -> dict: self._reset_issues() create_encoding_error = False create_custom_header_warning = False create_payload_error = False + create_payload_name_error = False if not properties: properties = {} # guard against None being passed in - i = random.randint(1, 3) + i = random.randint(1, 4) if simulate_errors and i == 1: create_encoding_error = True if simulate_errors and i == 2: create_custom_header_warning = True if simulate_errors and i == 3: create_payload_error = True + if simulate_errors and i == 4: + create_payload_name_error = True system_properties = self._parse_system_properties(message) @@ -94,6 +99,13 @@ def parse_message( message, origin_device_id, content_type, create_payload_error ) + self._validate_payload_against_dcm( + origin_device_id, + payload, + central_device_provider, + create_payload_name_error, + ) + event["payload"] = payload event_source = {"event": event} @@ -208,6 +220,24 @@ def _parse_content_type( return content_type + def _parse_annotations(self, message: Message): + try: + return unicode_binary_map(message.annotations) + except Exception: + self._warnings.append( + "Unable to decode message.annotations: {}".format(message.annotations) + ) + + def _parse_application_properties(self, message: Message): + try: + return unicode_binary_map(message.application_properties) + except Exception: + self._warnings.append( + "Unable to decode message.application_properties: {}".format( + message.application_properties + ) + ) + def _parse_payload( self, message: Message, origin_device_id, content_type, create_payload_error ): @@ -240,20 +270,50 @@ def _parse_payload( return payload - def _parse_annotations(self, message: Message): + def _validate_payload_against_dcm( + self, + origin_device_id: str, + payload: str, + central_device_provider: CentralDeviceProvider, + create_payload_name_error=False, + ): + if not central_device_provider: + return + + if not hasattr(payload, "keys"): + # some error happend while parsing + # should be captured by _parse_payload method above + return + try: - return unicode_binary_map(message.annotations) - except Exception: - self._warnings.append( - "Unable to decode message.annotations: {}".format(message.annotations) + template = central_device_provider.get_device_template(origin_device_id) + except Exception as e: + self._errors.append( + "Unable to get DCM for device: {}." + "Inner exception: {}".format(origin_device_id, e) ) + return - def _parse_application_properties(self, message: Message): - try: - return unicode_binary_map(message.application_properties) - except Exception: - self._warnings.append( - "Unable to decode message.application_properties: {}".format( - message.application_properties + all_schema = self._extract_schema_from_template(template) + all_names = [schema["name"] for schema in all_schema] + + for telemetry_name in payload.keys(): + if create_payload_name_error or telemetry_name not in all_names: + self._errors.append( + "Telemetry item '{}' is not present in DCM. " + "Device ID: {}. " + "List of allowed telemetry values for this type of device: {}. " + "NOTE: telemetry names are CASE-SENSITIVE".format( + telemetry_name, origin_device_id, all_names + ) ) - ) + + def _extract_schema_from_template(self, template): + all_schema = [] + dcm = template["capabilityModel"] + implements = dcm["implements"] + for implementation in implements: + contents = implementation["schema"]["contents"] + all_schema.extend(contents) + + return all_schema From b8044a5258ae72b089f9a717987147391d9b30bd Mon Sep 17 00:00:00 2001 From: prbans Date: Wed, 15 Apr 2020 12:03:38 -0700 Subject: [PATCH 07/11] Added unit test --- azext_iot/constants.py | 2 +- .../tests/central/json/device_template.json | 287 ++++++++++++++++++ azext_iot/tests/test_iot_utility_unit.py | 157 ++++++++-- 3 files changed, 417 insertions(+), 29 deletions(-) create mode 100644 azext_iot/tests/central/json/device_template.json diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 4054eddc6..0726d60ea 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.2" +VERSION = "0.9.4" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/tests/central/json/device_template.json b/azext_iot/tests/central/json/device_template.json new file mode 100644 index 000000000..5178f5b18 --- /dev/null +++ b/azext_iot/tests/central/json/device_template.json @@ -0,0 +1,287 @@ +{ + "id": "urn:d9cltbeus:tvj4oal1a0", + "etag": "\"~WgqHZmg+d95gTA53P8AnqBsDLGgj2wa0msOL7xozC9Y=\"", + "types": [ + "DeviceModel" + ], + "displayName": "duplicate-field-name", + "capabilityModel": { + "@id": "urn:sampleApp:modelOne_bz:2", + "@type": [ + "CapabilityModel" + ], + "implements": [ + { + "@id": "urn:sampleApp:modelOne_bz:_rpgcmdpo:1", + "@type": [ + "InterfaceInstance" + ], + "displayName": "Interface", + "name": "modelOne_g4", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:1", + "@type": [ + "Interface" + ], + "displayName": "Interface", + "contents": [ + { + "@id": "urn:sampleApp:modelOne_g4:Bool:1", + "@type": [ + "Telemetry" + ], + "displayName": "Bool", + "name": "Bool", + "schema": "boolean" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Date:1", + "@type": [ + "Telemetry" + ], + "displayName": "Date", + "name": "Date", + "schema": "date" + }, + { + "@id": "urn:sampleApp:modelOne_g4:DateTime:1", + "@type": [ + "Telemetry" + ], + "displayName": "DateTime", + "name": "DateTime", + "schema": "dateTime" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Double:1", + "@type": [ + "Telemetry" + ], + "displayName": "Double", + "name": "Double", + "schema": "double" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Duration:1", + "@type": [ + "Telemetry" + ], + "displayName": "Duration", + "name": "Duration", + "schema": "duration" + }, + { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:1", + "@type": [ + "Telemetry" + ], + "displayName": "IntEnum", + "name": "IntEnum", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:1", + "@type": [ + "Enum" + ], + "displayName": "Enum", + "valueSchema": "integer", + "enumValues": [ + { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum1:1", + "@type": [ + "EnumValue" + ], + "displayName": "Enum1", + "enumValue": 1, + "name": "Enum1" + }, + { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum2:1", + "@type": [ + "EnumValue" + ], + "displayName": "Enum2", + "enumValue": 2, + "name": "Enum2" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:1", + "@type": [ + "Telemetry" + ], + "displayName": "StringEnum", + "name": "StringEnum", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:1", + "@type": [ + "Enum" + ], + "displayName": "Enum", + "valueSchema": "string", + "enumValues": [ + { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumA:1", + "@type": [ + "EnumValue" + ], + "displayName": "EnumA", + "enumValue": "A", + "name": "EnumA" + }, + { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumB:1", + "@type": [ + "EnumValue" + ], + "displayName": "EnumB", + "enumValue": "B", + "name": "EnumB" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_g4:Float:1", + "@type": [ + "Telemetry" + ], + "displayName": "Float", + "name": "Float", + "schema": "float" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Geopoint:1", + "@type": [ + "Telemetry" + ], + "displayName": "Geopoint", + "name": "Geopoint", + "schema": "geopoint" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Int:1", + "@type": [ + "Telemetry" + ], + "displayName": "Int", + "name": "Int", + "schema": "integer" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Long:1", + "@type": [ + "Telemetry" + ], + "displayName": "Long", + "name": "Long", + "schema": "long" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Object:1", + "@type": [ + "Telemetry" + ], + "displayName": "Object", + "name": "Object", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:1", + "@type": [ + "Object" + ], + "displayName": "Object", + "fields": [ + { + "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:Double:1", + "@type": [ + "SchemaField" + ], + "displayName": "Double", + "name": "Double", + "schema": "double" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_g4:String:1", + "@type": [ + "Telemetry" + ], + "displayName": "String", + "name": "String", + "schema": "string" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Time:1", + "@type": [ + "Telemetry" + ], + "displayName": "Time", + "name": "Time", + "schema": "time" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Vector:1", + "@type": [ + "Telemetry" + ], + "displayName": "Vector", + "name": "Vector", + "schema": "vector" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_bz:myxqftpsr:2", + "@type": [ + "InterfaceInstance" + ], + "displayName": "Interface", + "name": "modelTwo_ed", + "schema": { + "@id": "urn:sampleApp:modelTwo_ed:1", + "@type": [ + "Interface" + ], + "displayName": "Interface", + "contents": [ + { + "@id": "urn:sampleApp:modelTwo_ed:Bool:1", + "@type": [ + "Telemetry" + ], + "displayName": "Bool", + "name": "Bool", + "schema": "boolean" + }, + { + "@id": "urn:sampleApp:modelTwo_ed:bool:1", + "@type": [ + "Telemetry" + ], + "displayName": "bool", + "name": "bool", + "schema": "boolean" + } + ] + } + } + ], + "displayName": "larger-telemetry-device", + "@context": [ + "http://azureiot.com/v1/contexts/IoTModel.json" + ] + }, + "solutionModel": { + "@id": "urn:d9cltbeus:lz1tl4a_jz", + "@type": [ + "SolutionModel" + ], + "cloudProperties": [], + "initialValues": [], + "overrides": [] + } +} \ No newline at end of file diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index c6399ca54..2a36b237e 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -1,36 +1,59 @@ -import pytest +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + import json +import mock +import pytest +import os + from knack.util import CLIError from uamqp.message import Message, MessageProperties from azure.cli.core.extension import get_extension_path -from azext_iot.common.utility import validate_min_python_version +from azext_iot.central.providers import CentralDeviceProvider +from azext_iot.common.utility import ( + validate_min_python_version, + process_json_arg, + read_file_content, + logger, +) from azext_iot.common.deps import ensure_uamqp -from azext_iot._validators import mode2_iot_login_handler from azext_iot.constants import EVENT_LIB, EXTENSION_NAME -from azext_iot.common.utility import process_json_arg, read_file_content, logger from azext_iot.operations.events3 import _parser +from azext_iot._validators import mode2_iot_login_handler + +_central_device_template_file = "central/json/device_template.json" + + +def load_json(filename): + from inspect import getsourcefile + + os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) + return json.loads(read_file_content(filename)) class TestMinPython(object): @pytest.mark.parametrize("pymajor, pyminor", [(3, 6), (3, 4), (2, 7)]) - def test_min_python(self, mocker, pymajor, pyminor): - version_mock = mocker.patch("azext_iot.common.utility.sys.version_info") - version_mock.major = pymajor - version_mock.minor = pyminor + def test_min_python(self, pymajor, pyminor): + with mock.patch("azext_iot.common.utility.sys.version_info") as version_mock: + version_mock.major = pymajor + version_mock.minor = pyminor - assert validate_min_python_version(2, 7) + assert validate_min_python_version(2, 7) @pytest.mark.parametrize( "pymajor, pyminor, exception", [(3, 6, SystemExit), (3, 4, SystemExit), (2, 7, SystemExit)], ) - def test_min_python_error(self, mocker, pymajor, pyminor, exception): - version_mock = mocker.patch("azext_iot.common.utility.sys.version_info") - version_mock.major = 2 - version_mock.minor = 6 + def test_min_python_error(self, pymajor, pyminor, exception): + with mock.patch("azext_iot.common.utility.sys.version_info") as version_mock: + version_mock.major = 2 + version_mock.minor = 6 - with pytest.raises(exception): - validate_min_python_version(pymajor, pyminor) + with pytest.raises(exception): + validate_min_python_version(pymajor, pyminor) class TestMode2Handler(object): @@ -269,13 +292,9 @@ def test_file_json_fail_invalidcontent(self, content, argname, set_cwd, mocker): assert mocked_util_logger.call_count == 0 -@pytest.mark.skipif( - not validate_min_python_version(3, 5, exit_on_fail=False), - reason="minimum python version not satisfied", -) class TestEvents3Parser: device_id = "some-device-id" - payload = {"someProperty": "someValue"} + payload = {"String": "someValue"} encoding = "UTF-8" content_type = "application/json" @@ -283,10 +302,12 @@ class TestEvents3Parser: bad_payload = "bad-payload" bad_content_type = "bad-content-type" + bad_dcm_payload = {"temperature": "someValue"} + def test_parse_message_should_succeed(self): # setup - app_prop_type = "some_app_prop" - app_prop_value = "some_app_value" + app_prop_type = "some_property" + app_prop_value = "some_value" properties = MessageProperties( content_encoding=self.encoding, content_type=self.content_type ) @@ -298,6 +319,10 @@ def test_parse_message_should_succeed(self): ) parser = _parser.Event3Parser() + device_template = load_json(_central_device_template_file) + provider = CentralDeviceProvider(cmd=None, app_id=None) + provider.get_device_template = mock.MagicMock(return_value=device_template) + # act parsed_msg = parser.parse_message( message=message, @@ -306,6 +331,7 @@ def test_parse_message_should_succeed(self): properties={"all"}, content_type_hint=None, simulate_errors=False, + central_device_provider=provider, ) # verify @@ -347,6 +373,7 @@ def test_parse_message_pnp_should_succeed(self): properties=None, content_type_hint=None, simulate_errors=False, + central_device_provider=None, ) # verify @@ -361,9 +388,7 @@ def test_parse_message_pnp_should_succeed(self): def test_parse_message_bad_content_type_should_warn(self): # setup encoded_payload = json.dumps(self.payload).encode() - properties = MessageProperties( - content_type=self.bad_content_type - ) + properties = MessageProperties(content_type=self.bad_content_type) message = Message( body=encoded_payload, properties=properties, @@ -372,7 +397,15 @@ def test_parse_message_bad_content_type_should_warn(self): parser = _parser.Event3Parser() # act - parsed_msg = parser.parse_message(message, None, None, None, None, False) + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties=None, + content_type_hint=None, + simulate_errors=False, + central_device_provider=None, + ) # verify # since the content_encoding header is not present, just dump the raw payload @@ -404,7 +437,15 @@ def test_parse_message_bad_encoding_should_fail(self): parser = _parser.Event3Parser() # act - parser.parse_message(message, None, None, None, None, False) + parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties=None, + content_type_hint=None, + simulate_errors=False, + central_device_provider=None, + ) assert len(parser._errors) == 1 assert len(parser._warnings) == 0 @@ -426,7 +467,15 @@ def test_parse_message_bad_json_should_fail(self): parser = _parser.Event3Parser() # act - parsed_msg = parser.parse_message(message, None, None, None, None, False) + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties=None, + content_type_hint=None, + simulate_errors=False, + central_device_provider=None, + ) # verify # parsing should attempt to place raw payload into result even if parsing fails @@ -466,6 +515,7 @@ def test_parse_message_pnp_should_fail(self): properties=None, content_type_hint=None, simulate_errors=False, + central_device_provider=None, ) # verify @@ -483,3 +533,54 @@ def test_parse_message_pnp_should_fail(self): self.device_id, expected_interface_name, actual_interface_name ) assert actual_error == expected_error + + def test_validate_against_template_should_fail(self): + # setup + app_prop_type = "some_property" + app_prop_value = "some_value" + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.bad_dcm_payload).encode(), + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + application_properties={app_prop_type.encode(): app_prop_value.encode()}, + ) + parser = _parser.Event3Parser() + + device_template = load_json(_central_device_template_file) + provider = CentralDeviceProvider(cmd=None, app_id=None) + provider.get_device_template = mock.MagicMock(return_value=device_template) + + # act + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties={"all"}, + content_type_hint=None, + simulate_errors=False, + central_device_provider=provider, + ) + + # verify + assert parsed_msg["event"]["payload"] == self.bad_dcm_payload + assert parsed_msg["event"]["origin"] == self.device_id + device_identifier = str(_parser.DEVICE_ID_IDENTIFIER, "utf8") + assert parsed_msg["event"]["annotations"][device_identifier] == self.device_id + + properties = parsed_msg["event"]["properties"] + assert properties["system"]["content_encoding"] == self.encoding + assert properties["system"]["content_type"] == self.content_type + assert properties["application"][app_prop_type] == app_prop_value + + assert len(parser._errors) == 1 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + actual_error = parser._errors[0] + expected_error = "Telemetry item '{}' is not present in DCM.".format( + list(self.bad_dcm_payload)[0] + ) + assert expected_error in actual_error From 3604d737e0da9e041d52d24734168c6de2358e88 Mon Sep 17 00:00:00 2001 From: prbans Date: Wed, 15 Apr 2020 13:19:54 -0700 Subject: [PATCH 08/11] Added provider tests --- .../central/providers/device_provider.py | 11 ++-- azext_iot/tests/central/json/device.json | 9 +++ azext_iot/tests/constants.py | 10 ++++ azext_iot/tests/helpers.py | 17 ++++++ azext_iot/tests/test_iot_central_unit.py | 59 ++++++++++++++++++- azext_iot/tests/test_iot_utility_unit.py | 16 ++--- 6 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 azext_iot/tests/central/json/device.json create mode 100644 azext_iot/tests/constants.py create mode 100644 azext_iot/tests/helpers.py diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 7922587ed..d655387e8 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -8,12 +8,6 @@ class CentralDeviceProvider: - _device_templates = {} - _devices = {} - _cmd = "" - _app_id = "" - _token = "" - def __init__(self, cmd, app_id, token=None): """ Provider for device/device_template APIs @@ -29,7 +23,8 @@ def __init__(self, cmd, app_id, token=None): self._cmd = cmd self._app_id = app_id self._token = token - pass + self._device_templates = {} + self._devices = {} def get_device_template( self, device_id, central_dns_suffix="azureiotcentral.com", @@ -42,6 +37,7 @@ def get_device_template( "No device template urn found for device '{}'".format(device_id) ) + # get or add to cache if ( device_template_urn not in self._device_templates or not self._device_templates.get(device_template_urn) @@ -70,6 +66,7 @@ def get_device( if not device_id: raise ValueError("Device id must be specified.") + # get or add to cache if device_id not in self._devices or not self._devices.get(device_id): self._devices[device_id] = central_services.device.get_device( self._cmd, device_id, self._app_id, self._token, central_dns_suffix diff --git a/azext_iot/tests/central/json/device.json b/azext_iot/tests/central/json/device.json new file mode 100644 index 000000000..0b138571a --- /dev/null +++ b/azext_iot/tests/central/json/device.json @@ -0,0 +1,9 @@ +{ + "id": "device-id", + "etag": "some-etag", + "displayName": "device-id", + "instanceOf": "urn:d9cltbeus:tvj4oal1a0", + "simulated": false, + "provisioned": true, + "approved": true +} \ No newline at end of file diff --git a/azext_iot/tests/constants.py b/azext_iot/tests/constants.py new file mode 100644 index 000000000..9519aa82e --- /dev/null +++ b/azext_iot/tests/constants.py @@ -0,0 +1,10 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +class FileNames: + central_device_template_file = "central/json/device_template.json" + central_device_file = "central/json/device.json" diff --git a/azext_iot/tests/helpers.py b/azext_iot/tests/helpers.py new file mode 100644 index 000000000..494ba4870 --- /dev/null +++ b/azext_iot/tests/helpers.py @@ -0,0 +1,17 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +import os + +from inspect import getsourcefile + +from azext_iot.common.utility import read_file_content + + +def load_json(filename): + os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) + return json.loads(read_file_content(filename)) diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index 23b60c533..fee7ad9ab 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -8,10 +8,14 @@ import pytest from knack.util import CLIError +from azure.cli.core.mock import DummyCli from azext_iot.operations import central as subject from azext_iot.common.shared import SdkType -from azure.cli.core.mock import DummyCli from azext_iot.common.utility import validate_min_python_version +from azext_iot.central.providers import CentralDeviceProvider + +from .helpers import load_json +from .constants import FileNames device_id = "mydevice" @@ -169,3 +173,56 @@ class TestMonitorEvents: def test_monitor_events_invalid_args(self, timeout, exception, fixture_cmd): with pytest.raises(exception): subject.iot_central_monitor_events(fixture_cmd, app_id, timeout=timeout) + + +@pytest.mark.skipif( + not validate_min_python_version(3, 5, exit_on_fail=False), + reason="minimum python version not satisfied", +) +class TestCentralDeviceProvider: + _device = load_json(FileNames.central_device_file) + _device_template = load_json(FileNames.central_device_template_file) + + @mock.patch("azext_iot.central.services.device_template") + @mock.patch("azext_iot.central.services.device") + def test_should_return_device(self, mock_device_svc, mock_device_template_svc): + # setup + provider = CentralDeviceProvider(cmd=None, app_id=app_id) + mock_device_svc.get_device.return_value = self._device + mock_device_template_svc.get_device_template.return_value = ( + self._device_template + ) + + # act + device = provider.get_device("someDeviceId") + # check that caching is working + device = provider.get_device("someDeviceId") + + # verify + # call counts should be at most 1 since the provider has a cache + assert mock_device_svc.get_device.call_count == 1 + assert mock_device_svc.get_device_template.call_count == 0 + assert device == self._device + + @mock.patch("azext_iot.central.services.device_template") + @mock.patch("azext_iot.central.services.device") + def test_should_return_device_template( + self, mock_device_svc, mock_device_template_svc + ): + # setup + provider = CentralDeviceProvider(cmd=None, app_id=app_id) + mock_device_svc.get_device.return_value = self._device + mock_device_template_svc.get_device_template.return_value = ( + self._device_template + ) + + # act + template = provider.get_device_template("someDeviceId") + # check that caching is working + template = provider.get_device_template("someDeviceId") + + # verify + # call counts should be at most 1 since the provider has a cache + assert mock_device_svc.get_device.call_count == 1 + assert mock_device_template_svc.get_device_template.call_count == 1 + assert template == self._device_template diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index 2a36b237e..feca08a65 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -7,7 +7,6 @@ import json import mock import pytest -import os from knack.util import CLIError from uamqp.message import Message, MessageProperties @@ -23,15 +22,8 @@ from azext_iot.constants import EVENT_LIB, EXTENSION_NAME from azext_iot.operations.events3 import _parser from azext_iot._validators import mode2_iot_login_handler - -_central_device_template_file = "central/json/device_template.json" - - -def load_json(filename): - from inspect import getsourcefile - - os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) - return json.loads(read_file_content(filename)) +from .helpers import load_json +from .constants import FileNames class TestMinPython(object): @@ -319,7 +311,7 @@ def test_parse_message_should_succeed(self): ) parser = _parser.Event3Parser() - device_template = load_json(_central_device_template_file) + device_template = load_json(FileNames.central_device_template_file) provider = CentralDeviceProvider(cmd=None, app_id=None) provider.get_device_template = mock.MagicMock(return_value=device_template) @@ -549,7 +541,7 @@ def test_validate_against_template_should_fail(self): ) parser = _parser.Event3Parser() - device_template = load_json(_central_device_template_file) + device_template = load_json(FileNames.central_device_template_file) provider = CentralDeviceProvider(cmd=None, app_id=None) provider.get_device_template = mock.MagicMock(return_value=device_template) From d368930551c4882d3932982684b2187c8df988db Mon Sep 17 00:00:00 2001 From: prbans Date: Wed, 15 Apr 2020 13:23:34 -0700 Subject: [PATCH 09/11] file rename --- azext_iot/tests/{constants.py => test_constants.py} | 0 azext_iot/tests/test_iot_central_unit.py | 2 +- azext_iot/tests/test_iot_utility_unit.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename azext_iot/tests/{constants.py => test_constants.py} (100%) diff --git a/azext_iot/tests/constants.py b/azext_iot/tests/test_constants.py similarity index 100% rename from azext_iot/tests/constants.py rename to azext_iot/tests/test_constants.py diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index fee7ad9ab..889e94372 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -15,7 +15,7 @@ from azext_iot.central.providers import CentralDeviceProvider from .helpers import load_json -from .constants import FileNames +from .test_constants import FileNames device_id = "mydevice" diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index feca08a65..c33e89b0a 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -23,7 +23,7 @@ from azext_iot.operations.events3 import _parser from azext_iot._validators import mode2_iot_login_handler from .helpers import load_json -from .constants import FileNames +from .test_constants import FileNames class TestMinPython(object): From 6d54316c694eb1f8eb74ffdb5e801e904bb0e5d5 Mon Sep 17 00:00:00 2001 From: prbans Date: Wed, 15 Apr 2020 15:57:05 -0700 Subject: [PATCH 10/11] Use CLIError for errors, no longer print --- azext_iot/central/providers/device_provider.py | 9 +++++---- azext_iot/central/services/device.py | 3 ++- azext_iot/central/services/device_template.py | 3 ++- azext_iot/constants.py | 2 +- azext_iot/operations/central.py | 4 +--- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index d655387e8..5583d7c64 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -4,6 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from knack.util import CLIError from azext_iot.central import services as central_services @@ -33,7 +34,7 @@ def get_device_template( device_template_urn = device["instanceOf"] if not device_template_urn: - raise ValueError( + raise CLIError( "No device template urn found for device '{}'".format(device_id) ) @@ -54,7 +55,7 @@ def get_device_template( device_template = self._device_templates.get(device_template_urn) if not device_template: - raise UnboundLocalError( + raise CLIError( "No device template for device with id: '{}'.".format(device_id) ) @@ -64,7 +65,7 @@ def get_device( self, device_id, central_dns_suffix="azureiotcentral.com", ): if not device_id: - raise ValueError("Device id must be specified.") + raise CLIError("Device id must be specified.") # get or add to cache if device_id not in self._devices or not self._devices.get(device_id): @@ -74,6 +75,6 @@ def get_device( device = self._devices.get(device_id) if not device: - raise UnboundLocalError("No device found with id: '{}'.".format(device_id)) + raise CLIError("No device found with id: '{}'.".format(device_id)) return device diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index 85fc7c983..4827b7a15 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -7,6 +7,7 @@ import requests +from knack.util import CLIError from ._utility import get_token @@ -45,6 +46,6 @@ def get_device( body = response.json() if "error" in body: - raise Exception(body["error"]) + raise CLIError(body["error"]) return body diff --git a/azext_iot/central/services/device_template.py b/azext_iot/central/services/device_template.py index fea6425bb..804ea6e0d 100644 --- a/azext_iot/central/services/device_template.py +++ b/azext_iot/central/services/device_template.py @@ -7,6 +7,7 @@ import requests +from knack.util import CLIError from ._utility import get_token @@ -45,6 +46,6 @@ def get_device_template( body = response.json() if "error" in body: - raise Exception(body["error"]) + raise CLIError(body["error"]) return body diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 0726d60ea..be276320f 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.4" +VERSION = "0.9.3" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index c95d89e68..5a471f4c2 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -38,9 +38,7 @@ def iot_central_device_capability_model_get( cmd, device_id, app_id, central_api_uri="api.azureiotcentral.com" ): provider = CentralDeviceProvider(cmd, app_id) - device_template = provider.get_device_template(device_id) - output = json.dumps(device_template, indent=4) - six.print_(output) + return provider.get_device_template(device_id) def iot_central_validate_messages( From ad56f4eb17f8c5d7f657a6e714c2159ab2bd6caf Mon Sep 17 00:00:00 2001 From: prbans Date: Wed, 15 Apr 2020 17:35:59 -0700 Subject: [PATCH 11/11] Address PR comments --- azext_iot/_help.py | 11 +++++ .../central/providers/device_provider.py | 4 +- azext_iot/central/services/_utility.py | 9 ++-- azext_iot/central/services/device.py | 9 ++-- azext_iot/central/services/device_template.py | 12 ++---- azext_iot/commands.py | 2 +- azext_iot/operations/central.py | 5 +-- azext_iot/operations/events3/_parser.py | 11 ++++- azext_iot/tests/test_iot_central_unit.py | 9 ---- azext_iot/tests/test_iot_utility_unit.py | 42 +++++++++++++++++++ 10 files changed, 78 insertions(+), 36 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index d40013c8a..1cf0ed68c 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -1247,6 +1247,17 @@ az iot central app validate-messages --app-id {app_id} --simulate-errors """ +helps[ + "iot central app capability-model show" +] = """ + type: command + short-summary: Get the device model from IoT central. + examples: + - name: Basic usage + text: > + az iot central app capability-model show --app-id {app_id} -d {device_id} + """ + helps[ "iot central device-twin" ] = """ diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 5583d7c64..330caec21 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -53,7 +53,7 @@ def get_device_template( central_dns_suffix, ) - device_template = self._device_templates.get(device_template_urn) + device_template = self._device_templates[device_template_urn] if not device_template: raise CLIError( "No device template for device with id: '{}'.".format(device_id) @@ -73,7 +73,7 @@ def get_device( self._cmd, device_id, self._app_id, self._token, central_dns_suffix ) - device = self._devices.get(device_id) + device = self._devices[device_id] if not device: raise CLIError("No device found with id: '{}'.".format(device_id)) diff --git a/azext_iot/central/services/_utility.py b/azext_iot/central/services/_utility.py index 2afa7e84a..5f01196e2 100644 --- a/azext_iot/central/services/_utility.py +++ b/azext_iot/central/services/_utility.py @@ -5,12 +5,13 @@ # -------------------------------------------------------------------------------------------- # Nothing in this file should be used outside of service/central +from azext_iot import constants from azext_iot.common import auth -def get_token(token, cmd): +def get_headers(token, cmd): if not token: - token = auth.get_aad_token(cmd, resource="https://apps.azureiotcentral.com") - return "Bearer {}".format(token["accessToken"]) + aad_token = auth.get_aad_token(cmd, resource="https://apps.azureiotcentral.com") + token = "Bearer {}".format(aad_token["accessToken"]) - return token + return {"Authorization": token, "User-Agent": constants.USER_AGENT} diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index 4827b7a15..de96af8a0 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -8,7 +8,7 @@ import requests from knack.util import CLIError -from ._utility import get_token +from . import _utility as utility def get_device( @@ -17,7 +17,7 @@ def get_device( app_id: str, token: str, central_dns_suffix="azureiotcentral.com", -) -> str: +) -> dict: """ Get device info given a device id @@ -33,13 +33,10 @@ def get_device( device: dict """ - if not token: - token = get_token(token, cmd) - url = "https://{}.{}/api/preview/devices/{}".format( app_id, central_dns_suffix, device_id ) - headers = {"Authorization": token} + headers = utility.get_headers(token, cmd) response = requests.get(url, headers=headers) diff --git a/azext_iot/central/services/device_template.py b/azext_iot/central/services/device_template.py index 804ea6e0d..de239f90a 100644 --- a/azext_iot/central/services/device_template.py +++ b/azext_iot/central/services/device_template.py @@ -3,12 +3,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devices +# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devicetemplates import requests from knack.util import CLIError -from ._utility import get_token +from . import _utility as utility def get_device_template( @@ -17,7 +17,7 @@ def get_device_template( app_id: str, token: str, central_dns_suffix="azureiotcentral.com", -) -> str: +) -> dict: """ Get device template given a device id @@ -32,14 +32,10 @@ def get_device_template( Returns: device: dict """ - - if not token: - token = get_token(token, cmd) - url = "https://{}.{}/api/preview/deviceTemplates/{}".format( app_id, central_dns_suffix, device_template_urn ) - headers = {"Authorization": token} + headers = utility.get_headers(token, cmd) response = requests.get(url, headers=headers) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 99c11e61a..fcb88dc99 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -192,7 +192,7 @@ def load_command_table(self, _): "iot central app capability-model", command_type=iotcentral_ops ) as cmd_group: cmd_group.command( - "get", "iot_central_device_capability_model_get", is_preview=True + "show", "iot_central_device_capability_model_show", is_preview=True ) with self.command_group( diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 5a471f4c2..a11b903b9 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -4,9 +4,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import json -import six - from knack.util import CLIError from azext_iot._factory import _bind_sdk from azext_iot.common._azure import get_iot_hub_token_from_central_app_id @@ -34,7 +31,7 @@ def iot_central_device_show( raise CLIError(unpack_msrest_error(e)) -def iot_central_device_capability_model_get( +def iot_central_device_capability_model_show( cmd, device_id, app_id, central_api_uri="api.azureiotcentral.com" ): provider = CentralDeviceProvider(cmd, app_id) diff --git a/azext_iot/operations/events3/_parser.py b/azext_iot/operations/events3/_parser.py index b9b932a29..72b37c4b3 100644 --- a/azext_iot/operations/events3/_parser.py +++ b/azext_iot/operations/events3/_parser.py @@ -294,8 +294,15 @@ def _validate_payload_against_dcm( ) return - all_schema = self._extract_schema_from_template(template) - all_names = [schema["name"] for schema in all_schema] + try: + all_schema = self._extract_schema_from_template(template) + all_names = [schema["name"] for schema in all_schema] + except Exception: + self._errors.append( + "Unable to extract device schema for device: {}." + "Template: {}".format(origin_device_id, template) + ) + return for telemetry_name in payload.keys(): if create_payload_name_error or telemetry_name not in all_names: diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index 889e94372..2365bcef3 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -11,7 +11,6 @@ from azure.cli.core.mock import DummyCli from azext_iot.operations import central as subject from azext_iot.common.shared import SdkType -from azext_iot.common.utility import validate_min_python_version from azext_iot.central.providers import CentralDeviceProvider from .helpers import load_json @@ -164,10 +163,6 @@ def test_device_twin_show_calls_get_twin( assert args[0] == ({"entity": resource}, SdkType.service_sdk) -@pytest.mark.skipif( - not validate_min_python_version(3, 5, exit_on_fail=False), - reason="minimum python version not satisfied", -) class TestMonitorEvents: @pytest.mark.parametrize("timeout, exception", [(-1, CLIError)]) def test_monitor_events_invalid_args(self, timeout, exception, fixture_cmd): @@ -175,10 +170,6 @@ def test_monitor_events_invalid_args(self, timeout, exception, fixture_cmd): subject.iot_central_monitor_events(fixture_cmd, app_id, timeout=timeout) -@pytest.mark.skipif( - not validate_min_python_version(3, 5, exit_on_fail=False), - reason="minimum python version not satisfied", -) class TestCentralDeviceProvider: _device = load_json(FileNames.central_device_file) _device_template = load_json(FileNames.central_device_template_file) diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index c33e89b0a..c4143098d 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -576,3 +576,45 @@ def test_validate_against_template_should_fail(self): list(self.bad_dcm_payload)[0] ) assert expected_error in actual_error + + def test_validate_against_bad_template_should_not_throw(self): + # setup + app_prop_type = "some_property" + app_prop_value = "some_value" + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.bad_dcm_payload).encode(), + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + application_properties={app_prop_type.encode(): app_prop_value.encode()}, + ) + parser = _parser.Event3Parser() + + provider = CentralDeviceProvider(cmd=None, app_id=None) + provider.get_device_template = mock.MagicMock( + return_value="an_unparseable_template" + ) + + # act + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties={"all"}, + content_type_hint=None, + simulate_errors=False, + central_device_provider=provider, + ) + + # verify + assert parsed_msg["event"]["payload"] == self.bad_dcm_payload + assert parsed_msg["event"]["origin"] == self.device_id + + assert len(parser._errors) == 1 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + actual_error = parser._errors[0] + assert "Unable to extract device schema for device" in actual_error