From 90013a7ce1e4cef93a4745d831d9d7d0b1b0481d Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Wed, 3 Oct 2018 14:06:06 -0700 Subject: [PATCH] Implement PnP and DigitalTwin commands --- azext_iot/__init__.py | 9 + azext_iot/_constants.py | 5 +- azext_iot/_factory.py | 10 +- azext_iot/_help.py | 250 ++++++ azext_iot/_params.py | 72 +- azext_iot/_validators.py | 3 + azext_iot/azext_metadata.json | 2 +- azext_iot/commands.py | 26 +- azext_iot/common/_azure.py | 88 ++ .../common/digitaltwin_sas_token_auth.py | 66 ++ azext_iot/common/shared.py | 21 + azext_iot/common/utility.py | 38 +- azext_iot/operations/_mqtt.py | 4 +- azext_iot/operations/digitaltwin.py | 278 ++++++ azext_iot/operations/dps.py | 5 +- azext_iot/operations/events3/_events.py | 58 +- azext_iot/operations/generic.py | 2 +- azext_iot/operations/hub.py | 73 +- azext_iot/operations/pnp.py | 240 ++++++ azext_iot/pnp_sdk/__init__.py | 18 + .../digital_twin_repository_service.py | 360 ++++++++ azext_iot/pnp_sdk/models/__init__.py | 25 + azext_iot/pnp_sdk/models/model_information.py | 72 ++ .../pnp_sdk/models/model_information_py3.py | 72 ++ azext_iot/pnp_sdk/models/search_options.py | 42 + .../pnp_sdk/models/search_options_py3.py | 42 + azext_iot/pnp_sdk/models/search_response.py | 33 + .../pnp_sdk/models/search_response_py3.py | 33 + azext_iot/pnp_sdk/version.py | 13 + .../iot_hub_gateway_service_apis.py | 375 ++++++++- azext_iot/service_sdk/models/__init__.py | 20 + azext_iot/service_sdk/models/desired.py | 29 + azext_iot/service_sdk/models/desired_state.py | 40 + .../models/digital_twin_interfaces.py | 32 + .../models/digital_twin_interfaces_patch.py | 28 + ..._twin_interfaces_patch_interfaces_value.py | 28 + ...patch_interfaces_value_properties_value.py | 28 + ...terfaces_value_properties_value_desired.py | 29 + azext_iot/service_sdk/models/interface.py | 32 + azext_iot/service_sdk/models/property.py | 32 + azext_iot/service_sdk/models/reported.py | 32 + azext_iot/service_sdk/version.py | 3 +- .../test_device_digitaltwin_interfaces.json | 58 ++ ...est_device_digitaltwin_invoke_command.json | 5 + ...st_device_digitaltwin_property_update.json | 11 + azext_iot/tests/test_iot_digitaltwin_unit.py | 417 +++++++++ azext_iot/tests/test_iot_ext_int.py | 4 +- azext_iot/tests/test_iot_pnp_int.py | 174 ++++ azext_iot/tests/test_iot_pnp_unit.py | 791 ++++++++++++++++++ .../test_pnp_create_payload_interface.json | 210 +++++ .../tests/test_pnp_create_payload_model.json | 12 + azext_iot/tests/test_pnp_interface_list.json | 19 + azext_iot/tests/test_pnp_interface_show.json | 89 ++ scripts/ci/test_source.sh | 6 + scripts/ci/test_static.sh | 2 +- scripts/ci/test_static_py2.sh | 2 +- 56 files changed, 4407 insertions(+), 61 deletions(-) create mode 100644 azext_iot/common/digitaltwin_sas_token_auth.py create mode 100644 azext_iot/operations/digitaltwin.py create mode 100644 azext_iot/operations/pnp.py create mode 100644 azext_iot/pnp_sdk/__init__.py create mode 100644 azext_iot/pnp_sdk/digital_twin_repository_service.py create mode 100644 azext_iot/pnp_sdk/models/__init__.py create mode 100644 azext_iot/pnp_sdk/models/model_information.py create mode 100644 azext_iot/pnp_sdk/models/model_information_py3.py create mode 100644 azext_iot/pnp_sdk/models/search_options.py create mode 100644 azext_iot/pnp_sdk/models/search_options_py3.py create mode 100644 azext_iot/pnp_sdk/models/search_response.py create mode 100644 azext_iot/pnp_sdk/models/search_response_py3.py create mode 100644 azext_iot/pnp_sdk/version.py create mode 100644 azext_iot/service_sdk/models/desired.py create mode 100644 azext_iot/service_sdk/models/desired_state.py create mode 100644 azext_iot/service_sdk/models/digital_twin_interfaces.py create mode 100644 azext_iot/service_sdk/models/digital_twin_interfaces_patch.py create mode 100644 azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value.py create mode 100644 azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py create mode 100644 azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py create mode 100644 azext_iot/service_sdk/models/interface.py create mode 100644 azext_iot/service_sdk/models/property.py create mode 100644 azext_iot/service_sdk/models/reported.py create mode 100644 azext_iot/tests/test_device_digitaltwin_interfaces.json create mode 100644 azext_iot/tests/test_device_digitaltwin_invoke_command.json create mode 100644 azext_iot/tests/test_device_digitaltwin_property_update.json create mode 100644 azext_iot/tests/test_iot_digitaltwin_unit.py create mode 100644 azext_iot/tests/test_iot_pnp_int.py create mode 100644 azext_iot/tests/test_iot_pnp_unit.py create mode 100644 azext_iot/tests/test_pnp_create_payload_interface.json create mode 100644 azext_iot/tests/test_pnp_create_payload_model.json create mode 100644 azext_iot/tests/test_pnp_interface_list.json create mode 100644 azext_iot/tests/test_pnp_interface_show.json diff --git a/azext_iot/__init__.py b/azext_iot/__init__.py index b7b439a25..f44bb927a 100644 --- a/azext_iot/__init__.py +++ b/azext_iot/__init__.py @@ -20,8 +20,17 @@ client_factory=iot_service_provisioning_factory ) +iotdigitaltwin_ops = CliCommandType( + operations_tmpl='azext_iot.operations.digitaltwin#{}' +) + +iotpnp_ops = CliCommandType( + operations_tmpl='azext_iot.operations.pnp#{}' +) + class IoTExtCommandsLoader(AzCommandsLoader): + def __init__(self, cli_ctx=None): super(IoTExtCommandsLoader, self).__init__(cli_ctx=cli_ctx) diff --git a/azext_iot/_constants.py b/azext_iot/_constants.py index 98e7689ca..afd8c90d5 100644 --- a/azext_iot/_constants.py +++ b/azext_iot/_constants.py @@ -6,7 +6,7 @@ import os -VERSION = '0.7.1' +VERSION = '0.8.0' EXTENSION_NAME = 'azure-cli-iot-ext' EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = 'iotext' @@ -16,6 +16,9 @@ MIN_SIM_MSG_INTERVAL = 1 MIN_SIM_MSG_COUNT = 1 SIM_RECEIVE_SLEEP_SEC = 3 +PNP_API_VERSION = '2019-07-01-preview' +PNP_ENDPOINT = 'https://provider.azureiotrepository.com' +PNP_REPO_ENDPOINT = 'https://repo.azureiotrepository.com' DEVICE_DEVICESCOPE_PREFIX = 'ms-azure-iot-edge://' TRACING_PROPERTY = 'azureiot*com^dtracing^1' TRACING_ALLOWED_FOR_LOCATION = ('northeurope', 'westus2', 'west us 2', 'southeastasia') diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py index 0500ba5e3..2f8f8f2fe 100644 --- a/azext_iot/_factory.py +++ b/azext_iot/_factory.py @@ -55,6 +55,7 @@ def _bind_sdk(target, sdk_type, device_id=None): from azext_iot.custom_sdk.custom_api import CustomClient from azext_iot.dps_sdk import ProvisioningServiceClient + from azext_iot.pnp_sdk.digital_twin_repository_service import DigitalTwinRepositoryService sas_uri = target['entity'] endpoint = "https://{}".format(sas_uri) @@ -97,6 +98,12 @@ def _bind_sdk(target, sdk_type, device_id=None): _get_sdk_exception_type(sdk_type) ) + if sdk_type is SdkType.pnp_sdk: + return ( + DigitalTwinRepositoryService(endpoint), + _get_sdk_exception_type(sdk_type) + ) + return None @@ -107,6 +114,7 @@ def _get_sdk_exception_type(sdk_type): SdkType.custom_sdk: import_module('azext_iot.custom_sdk.models.error_details'), SdkType.service_sdk: import_module('msrestazure.azure_exceptions'), SdkType.device_sdk: import_module('msrestazure.azure_exceptions'), - SdkType.dps_sdk: import_module('azext_iot.dps_sdk.models.provisioning_service_error_details') + SdkType.dps_sdk: import_module('azext_iot.dps_sdk.models.provisioning_service_error_details'), + SdkType.pnp_sdk: import_module('msrest.exceptions') } return exception_library.get(sdk_type, None) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 068d668b4..4ff15b9ab 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -912,3 +912,253 @@ type: command short-summary: Delete a device registration in an Azure IoT Hub Device Provisioning Service. """ + +helps['iot dt'] = """ + type: group + short-summary: Manage digital twin of an IoT Plug and Play device. +""" + +helps['iot dt invoke-command'] = """ + type: command + short-summary: Executes a command on an IoT Plug and Play device. + long-summary: You can leverage az login and provide --hub-name instead of --login for every command. + examples: + - name: Execute a command on device . + text: > + az iot dt invoke-command --login {iothub_cs} + --interface {plug_and_play_interface} --device-id {device_id} + --command-name {command_name} --command-payload {payload} + - name: Execute a command on device within current session. + text: > + az iot dt invoke-command --hub-name {iothub_name} + --interface {plug_and_play_interface} --device-id {device_id} + --command-name {command_name} --command-payload {payload} +""" + +helps['iot dt list-interfaces'] = """ + type: command + short-summary: List interfaces of a target IoT Plug and Play device. + long-summary: You can leverage az login and provide --hub-name instead of --login for every command. + examples: + - name: List all IoT Plug and Play interfaces on a device. + text: > + az iot dt list-interfaces --login {iothub_cs} + --device-id {device_id} + - name: List all IoT Plug and Play interfaces on a device within current session. + text: > + az iot dt list-interfaces --hub-name {iothub_name} --device-id {device_id} +""" + +helps['iot dt list-properties'] = """ + type: command + short-summary: List properties of a target IoT Plug and Play device interface(s). + long-summary: You can leverage az login and provide --hub-name instead of --login for every command. + examples: + - name: List all properties of all device's interfaces on an IoT Plug and Play device. + text: > + az iot dt list-properties --login {iothub_cs} --source device + --device-id {device_id} + - name: List all properties of all public interfaces on an IoT Plug and Play device within current session. + text: > + az iot dt list-properties --hub-name {iothub_name} --device-id {device_id} --source public + - name: List all properties of device's interface on an IoT Plug and Play device. + text: > + az iot dt list-properties --login {iothub_cs} --source device + --device-id {device_id} --interface {plug_and_play_interface} +""" + +helps['iot dt list-commands'] = """ + type: command + short-summary: List commands of an IoT Plug and Play devices interface(s). + long-summary: You can leverage az login and provide --hub-name instead of --login for every command. + examples: + - name: List all commands of all private interfaces on an IoT Plug and Play device. + text: > + az iot dt list-commands --login {iothub_cs} --source private + --device-id {device_id} --repo-id {plug_and_play_model_repository_id} + - name: List all commands of a private interface on an IoT Plug and Play device. + text: > + az iot dt list-commands --login {iothub_cs} --source private + --device-id {device_id} --repo-id {plug_and_play_model_repository_id} + --interface {plug_and_play_interface} + - name: List all commands of all public interfaces on an IoT Plug and Play device. + text: > + az iot dt list-commands --login {iothub_cs} --source public + --device-id {device_id} + - name: List all commands of device's interface on an IoT Plug and Play device. + text: > + az iot dt list-commands --login {iothub_cs} --source device + --device-id {device_id} --interface {plug_and_play_interface} +""" + +helps['iot dt monitor-events'] = """ + type: command + short-summary: Monitor digital twin events. + long-summary: You can leverage az login and provide --hub-name instead of --login for every command. + examples: + - name: Monitor digital twin events of device's interface. + text: > + az iot dt monitor-events --login {iothub_cs} --device-id {device_id} --source device + --interface {plug_and_play_interface} --consumer-group {consumer_group_name} + - name: Monitor digital twin events of public interface within current session. + text: > + az iot dt monitor-events --hub-name {iothub_name} --device-id {device_id} --source public + --interface {plug_and_play_interface} --consumer-group {consumer_group_name} + - name: Monitor digital twin events of device's interface and see all message properties. + text: > + az iot dt monitor-events --login {iothub_cs} --device-id {device_id} + --interface {plug_and_play_interface} --consumer-group {consumer_group_name} + --properties all --source device +""" + +helps['iot dt update-property'] = """ + type: command + short-summary: Update an IoT Plug and Play device interfaces writable property. + examples: + - name: Update an IoT Plug and Play device interfaces read-write property. + text: > + az iot dt update-property --login {iothub_cs} --device-id {device_id} + --interface-payload {payload} + - name: Update an IoT Plug and Play device interfaces read-write property within current session. + text: > + az iot dt update-property --hub-name {iothub_name} --device-id {device_id} + --interface-payload {payload} +""" + +helps['iot pnp'] = """ + type: group + short-summary: Manage entities of an IoT Plug and Play model repository. +""" + +helps['iot pnp interface'] = """ + type: group + short-summary: Manage interfaces in an IoT Plug and Play model repository. +""" + +helps['iot pnp interface publish'] = """ + type: command + short-summary: Publish an interface to public repository. + examples: + - name: Publish an interface to public repository. + text: > + az iot pnp interface publish -r {pnp_repository} --interface {plug_and_play_interface_id} +""" + +helps['iot pnp interface create'] = """ + type: command + short-summary: Create an interface in the company repository. + examples: + - name: Create an interface in the company repository. + text: > + az iot pnp interface create --def {plug_and_play_interface_file_path} -r {pnp_repository} +""" + +helps['iot pnp interface update'] = """ + type: command + short-summary: Update an interface in the company repository. + examples: + - name: Update an interface in the company repository. + text: > + az iot pnp interface update --def {updated_plug_and_play_interface_file_path} -r {pnp_repository} +""" + +helps['iot pnp interface list'] = """ + type: command + short-summary: List all interfaces. + examples: + - name: List all company repository's interfaces. + text: > + az iot pnp interface list -r {pnp_repository} + - name: List all public interfaces. + text: > + az iot pnp interface list +""" + +helps['iot pnp interface show'] = """ + type: command + short-summary: Get the details of an interface. + examples: + - name: Get the details of a company repository interface. + text: > + az iot pnp interface show -r {pnp_repository} --interface {plug_and_play_interface_id} + - name: Get the details of public interface. + text: > + az iot pnp interface show --interface {plug_and_play_interface_id} +""" + +helps['iot pnp interface delete'] = """ + type: command + short-summary: Delete an interface in the company repository. + examples: + - name: Delete an interface in the company repository. + text: > + az iot pnp interface delete -r {pnp_repository} --interface {plug_and_play_interface_id} +""" + +helps['iot pnp capability-model'] = """ + type: group + short-summary: Manage device capability models in an IoT Plug and Play model repository. +""" + +helps['iot pnp capability-model list'] = """ + type: command + short-summary: List all capability-model. + examples: + - name: List all company repository's capability-model. + text: > + az iot pnp capability-model list -r {pnp_repository} + - name: List all public capability-model. + text: > + az iot pnp capability-model list +""" + +helps['iot pnp capability-model show'] = """ + type: command + short-summary: Get the details of a capability-model. + examples: + - name: Get the details of a company repository capability-model. + text: > + az iot pnp capability-model show -r {pnp_repository} --model {plug_and_play_capability_model_id} + - name: Get the details of public capability-model. + text: > + az iot pnp capability-model show --model {plug_and_play_capability_model_id} +""" + +helps['iot pnp capability-model create'] = """ + type: command + short-summary: Create a capability-model in the company repository. + examples: + - name: Create a capability-model in the company repository. + text: > + az iot pnp capability-model create --def {plug_and_play_capability_model_file_path} -r {pnp_repository} +""" + +helps['iot pnp capability-model publish'] = """ + type: command + short-summary: Publish the capability-model to public repository. + examples: + - name: Publish the capability-model to public repository. + text: > + az iot pnp capability-model publish -r {pnp_repository} + --model {plug_and_play_capability_model_id} +""" + +helps['iot pnp capability-model delete'] = """ + type: command + short-summary: Delete the capability-model in the company repository. + examples: + - name: Delete the capability-model in the company repository. + text: > + az iot pnp capability-model delete -r {pnp_repository} + --model {plug_and_play_capability_model_id} +""" + +helps['iot pnp capability-model update'] = """ + type: command + short-summary: Update the capability-model in the company repository. + examples: + - name: Update the capability-model in the company repository. + text: > + az iot pnp capability-model update --def {updated_plug_and_play_capability_model_file_path} + -r {pnp_repository} +""" diff --git a/azext_iot/_params.py b/azext_iot/_params.py index dcee8d2ff..10c96ff75 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -26,7 +26,8 @@ MetricType, ReprovisionType, AllocationType, - DistributedTracingSamplingModeType + DistributedTracingSamplingModeType, + ModelSourceType ) from azext_iot._validators import mode2_iot_login_handler @@ -87,6 +88,8 @@ def load_arguments(self, _): context.argument('repair', options_list=['--repair', '-r'], arg_type=get_three_state_flag(), help='Reinstall uamqp dependency compatible with extension version. Default: false') + context.argument('repo_endpoint', options_list=['--endpoint', '-e'], help='IoT Plug and Play endpoint.') + context.argument('repo_id', options_list=['--repo-id', '-r'], help='IoT Plug and Play repository Id.') with self.argument_context('iot hub') as context: context.argument('target_json', options_list=['--json', '-j'], @@ -378,3 +381,70 @@ def load_arguments(self, _): with self.argument_context('iot dps registration list') as context: context.argument('enrollment_id', help='ID of enrollment group') + + with self.argument_context('iot dt') as context: + context.argument('repo_login', options_list=['--repo-login', '--rl'], + help='This command supports an entity connection string with rights to perform action. ' + 'Use to avoid PnP endpoint and repository name if repository is private. ' + 'If both an entity connection string and name are provided the connection string takes priority.') + context.argument('interface', options_list=['--interface', '-i'], + help='Target interface name. This should be the name of the interface not the urn-id.') + context.argument('command_name', options_list=['--command-name', '--cn'], + help='IoT Plug and Play interface command name.') + context.argument('command_payload', options_list=['--command-payload', '--cp', '--cv'], + help='IoT Plug and Play interface command payload. ' + 'Content can be directly input or extracted from a file path.') + context.argument('interface_payload', options_list=['--interface-payload', '--ip', '--iv'], + help='IoT Plug and Play interface payload. ' + 'Content can be directly input or extracted from a file path.') + context.argument('source_model', options_list=['--source', '-s'], + help='Choose your option to get model definition from specified source. ', + arg_type=get_enum_type(ModelSourceType)) + context.argument('schema', options_list=['--schema'], + help='Show interface with entity schema.') + + with self.argument_context('iot dt monitor-events') as context: + context.argument('consumer_group', options_list=['--consumer-group', '--cg'], + help='Specify the consumer group to use when connecting to event hub endpoint.') + context.argument('properties', options_list=['--properties', '--props', '-p'], arg_type=event_msg_prop_type) + context.argument('pnp_context', options_list=['--pnp-context'], + arg_type=get_three_state_flag(), + help='Plug and Play telemetry context.') + context.argument('repair', options_list=['--repair'], + arg_type=get_three_state_flag(), + help='Reinstall uamqp dependency compatible with extension version. Default: false') + + with self.argument_context('iot pnp') as context: + context.argument('model', options_list=['--model', '-m'], + help='Target capability-model urn-id. Example: urn:example:capabilityModels:Mxchip:1') + context.argument('interface', options_list=['--interface', '-i'], + help='Target interface urn-id. Example: urn:example:interfaces:MXChip:1') + + with self.argument_context('iot pnp interface') as context: + context.argument('interface_definition', options_list=['--definition', '--def'], + help='IoT Plug and Play interface definition written in PPDL (JSON-LD). ' + 'Can be directly input or a file path where the content is extracted.') + + with self.argument_context('iot pnp interface list') as context: + context.argument('search_string', options_list=['--search', '--ss'], + help='Searches IoT Plug and Play interfaces for given string in the' + ' \"Description, DisplayName, comment and Id\".') + context.argument('top', type=int, options_list=['--top'], + help='Maximum number of interface to return.') + + with self.argument_context('iot pnp capability-model') as context: + context.argument('model_definition', options_list=['--definition', '--def'], + help='IoT Plug and Play capability-model definition written in PPDL (JSON-LD). ' + 'Can be directly input or a file path where the content is extracted.') + + with self.argument_context('iot pnp capability-model show') as context: + context.argument('expand', options_list=['--expand'], + help='Indicates whether to expand the device capability model\'s' + ' interface definitions or not.') + + with self.argument_context('iot pnp capability-model list') as context: + context.argument('search_string', options_list=['--search', '--ss'], + help='Searches IoT Plug and Play models for given string in the' + ' \"Description, DisplayName, comment and Id\".') + context.argument('top', type=int, options_list=['--top'], + help='Maximum number of capability-model to return.') diff --git a/azext_iot/_validators.py b/azext_iot/_validators.py index 4cdb3355e..95cb23acf 100644 --- a/azext_iot/_validators.py +++ b/azext_iot/_validators.py @@ -22,6 +22,9 @@ def mode2_iot_login_handler(cmd, namespace): elif 'dps_name' in arg_keys: iot_cmd_type = 'DPS' entity_value = args.get('dps_name') + elif 'repo_endpoint' in arg_keys: + iot_cmd_type = 'PnP' + entity_value = args.get('repo_endpoint') if not any([login_value, entity_value]): raise CLIError(ERROR_NO_HUB_OR_LOGIN_ON_INPUT(iot_cmd_type)) diff --git a/azext_iot/azext_metadata.json b/azext_iot/azext_metadata.json index 0fa9ed9de..90c1960c4 100644 --- a/azext_iot/azext_metadata.json +++ b/azext_iot/azext_metadata.json @@ -1,4 +1,4 @@ { "azext.minCliCoreVersion": "2.0.24", - "version": "0.7.1" + "version": "0.8.0" } \ No newline at end of file diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 32dc43fbf..0ab883b9b 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -9,7 +9,7 @@ Load CLI commands """ -from azext_iot import iothub_ops, iotdps_ops +from azext_iot import iothub_ops, iotdps_ops, iotdigitaltwin_ops, iotpnp_ops def load_command_table(self, _): @@ -120,3 +120,27 @@ def load_command_table(self, _): cmd_group.command('list', 'iot_dps_registration_list') cmd_group.command('show', 'iot_dps_registration_get') cmd_group.command('delete', 'iot_dps_registration_delete') + + with self.command_group('iot dt', command_type=iotdigitaltwin_ops) as cmd_group: + cmd_group.command('list-interfaces', 'iot_digitaltwin_interface_list') + cmd_group.command('list-properties', 'iot_digitaltwin_properties_list') + cmd_group.command('update-property', 'iot_digitaltwin_property_update') + cmd_group.command('invoke-command', 'iot_digitaltwin_invoke_command') + cmd_group.command('monitor-events', 'iot_digitaltwin_monitor_events') + cmd_group.command('list-commands', 'iot_digitaltwin_command_list') + + with self.command_group('iot pnp interface', command_type=iotpnp_ops) as cmd_group: + cmd_group.command('show', 'iot_pnp_interface_show') + cmd_group.command('list', 'iot_pnp_interface_list') + cmd_group.command('create', 'iot_pnp_interface_create') + cmd_group.command('publish', 'iot_pnp_interface_publish') + cmd_group.command('delete', 'iot_pnp_interface_delete') + cmd_group.command('update', 'iot_pnp_interface_update') + + with self.command_group('iot pnp capability-model', command_type=iotpnp_ops) as cmd_group: + cmd_group.command('show', 'iot_pnp_model_show') + cmd_group.command('list', 'iot_pnp_model_list') + cmd_group.command('create', 'iot_pnp_model_create') + cmd_group.command('publish', 'iot_pnp_model_publish') + cmd_group.command('delete', 'iot_pnp_model_delete') + cmd_group.command('update', 'iot_pnp_model_update') diff --git a/azext_iot/common/_azure.py b/azext_iot/common/_azure.py index da6308de9..ae58b831a 100644 --- a/azext_iot/common/_azure.py +++ b/azext_iot/common/_azure.py @@ -21,6 +21,11 @@ def _parse_connection_string(cs, validate=None, cstring_type='entity'): return decomposed +def parse_pnp_connection_string(cs): + validate = ['HostName', 'RepositoryId', 'SharedAccessKeyName', 'SharedAccessKey'] + return _parse_connection_string(cs, validate, 'PnP Model Repository') + + def parse_iot_hub_connection_string(cs): validate = ['HostName', 'SharedAccessKeyName', 'SharedAccessKey'] return _parse_connection_string(cs, validate, 'IoT Hub') @@ -217,3 +222,86 @@ def _find_iot_dps_from_list(all_dps, dps_name): result['subscription'] = client.config.subscription_id return result + + +# pylint: disable=broad-except +def get_iot_pnp_connection_string( + cmd, + endpoint, + repo_id, + user_role='Admin', + login=None): + """ + Function used to build up dictionary of IoT PnP connection string parts + + Args: + cmd (object): Knack cmd + endpoint (str): PnP endpoint + repository_id (str): PnP repository Id. + user_role (str): User role of the access key for the given PnP repository. + + Returns: + (dict): of connection string elements. + + Raises: + CLIError: on input validation failure. + + """ + + # pylint: disable=line-too-long + from azure.cli.command_modules.iot.digitaltwinrepositoryprovisioningservice import DigitalTwinRepositoryProvisioningService + from azure.cli.command_modules.iot._utils import get_auth_header + from azext_iot._constants import PNP_REPO_ENDPOINT + + result = {} + client = None + headers = None + + if login: + + try: + decomposed = parse_pnp_connection_string(login) + except ValueError as e: + raise CLIError(e) + + result = {} + result['cs'] = login + result['policy'] = decomposed['SharedAccessKeyName'] + result['primarykey'] = decomposed['SharedAccessKey'] + result['repository_id'] = decomposed['RepositoryId'] + result['entity'] = decomposed['HostName'] + result['entity'] = result['entity'].replace('https://', '') + result['entity'] = result['entity'].replace('http://', '') + return result + + def _find_key_from_list(keys, user_role): + if keys: + return next((key for key in keys if key.user_role.lower() == user_role.lower()), None) + return None + + if repo_id: + client = DigitalTwinRepositoryProvisioningService(endpoint) + headers = get_auth_header(cmd) + keys = client.get_keys_async(repository_id=repo_id, api_version=client.api_version, custom_headers=headers) + + if keys is None: + raise CLIError('Auth key required for repository "{}"'.format(repo_id)) + + policy = _find_key_from_list(keys, user_role) + + if policy is None: + raise CLIError( + 'No auth key found for repository "{}" with user_role "{}".'.format(repo_id, user_role) + ) + + result['cs'] = policy.connection_string + result['entity'] = policy.service_endpoint + result['policy'] = policy.id + result['primarykey'] = policy.secret + result['repository_id'] = policy.repository_id + else: + result['entity'] = PNP_REPO_ENDPOINT + + result['entity'] = result['entity'].replace('https://', '') + result['entity'] = result['entity'].replace('http://', '') + return result diff --git a/azext_iot/common/digitaltwin_sas_token_auth.py b/azext_iot/common/digitaltwin_sas_token_auth.py new file mode 100644 index 000000000..eb98a74ce --- /dev/null +++ b/azext_iot/common/digitaltwin_sas_token_auth.py @@ -0,0 +1,66 @@ +# 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. +# -------------------------------------------------------------------------------------------- +# pylint: disable=too-few-public-methods +""" +digitaltwin_sas_token_auth: Module containing DigitalTwin Model Shared Access Signature token class. + +""" + +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) # pylint: disable=import-error +from msrest.authentication import Authentication + + +class DigitalTwinSasTokenAuthentication(Authentication): + """ + Shared Access Signature authorization for DigitalTwin Repository. + + Args: + uri (str): Uri of target resource. + shared_access_policy_name (str): Name of shared access policy. + shared_access_key (str): Shared access key. + expiry (int): Expiry of the token to be generated. Input should + be seconds since the epoch, in UTC. Default is an hour later from now. + """ + def __init__(self, repositoryId, endpoint, shared_access_key_name, shared_access_key, expiry=None): + self.repositoryId = repositoryId + self.policy = shared_access_key_name + self.key = shared_access_key + self.endpoint = endpoint + if expiry is None: + self.expiry = time() + 3600 # Default expiry is an hour later + else: + self.expiry = expiry + + def generate_sas_token(self): + """ + Create a shared access signiture token as a string literal. + + Returns: + result (str): SAS token as string literal. + """ + encoded_uri = quote_plus(self.endpoint) + encoded_repo_id = quote_plus(self.repositoryId) + ttl = int(self.expiry) + sign_key = '%s\n%s\n%d' % (encoded_repo_id, encoded_uri, ttl) + signature = b64encode(HMAC(b64decode(self.key), sign_key.encode('utf-8'), sha256).digest()) + result = { + 'sr': self.endpoint, + 'sig': signature, + 'se': str(ttl) + } + + if self.policy: + result['skn'] = self.policy + result['rid'] = self.repositoryId + + return 'SharedAccessSignature ' + urlencode(result) diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index 0de70ed61..15e9dce51 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -25,6 +25,7 @@ class SdkType(Enum): dps_sdk = 5 device_sdk = 6 service_sdk = 7 + pnp_sdk = 8 # pylint: disable=too-few-public-methods @@ -139,3 +140,23 @@ class DistributedTracingSamplingModeType(Enum): """ off = 'off' on = 'on' + + +# pylint: disable=too-few-public-methods +class PnPModelType(Enum): + """ + Type of PnP Model. + """ + any = 'any' + interface = 'Interface' + capabilityModel = 'capabilityModel' + + +# pylint: disable=too-few-public-methods +class ModelSourceType(Enum): + """ + Type of source to get model definition. + """ + public = 'public' + private = 'private' + device = 'device' diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index a4f5985a5..cc4f4cc2c 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -259,15 +259,29 @@ def test_import(package): return True +def unpack_pnp_http_error(e): + error = unpack_msrest_error(e) + if isinstance(error, dict): + if error.get('error'): + error = error['error'] + if error.get('stackTrace'): + error.pop('stackTrace') + return error + + def unpack_msrest_error(e, clouderror=True): """ Obtains full response text from an msrest error """ if clouderror: + op_err = None try: - return json.loads(e.response.text) + op_err = json.loads(e.response.text) except ValueError: - return e.response.text + op_err = e.response.text except TypeError: - return e.response.text + op_err = e.response.text + if not op_err: + return str(e) + return op_err return e @@ -280,3 +294,21 @@ def calculate_millisec_since_unix_epoch_utc(): now = datetime.utcnow() epoch = datetime.utcfromtimestamp(0) return int(1000 * (now - epoch).total_seconds()) + + +def get_sas_token(target): + from azext_iot.common.digitaltwin_sas_token_auth import DigitalTwinSasTokenAuthentication + token = '' + if target.get('repository_id'): + token = DigitalTwinSasTokenAuthentication(target["repository_id"], + target["entity"], + target["policy"], + target["primarykey"]).generate_sas_token() + return {'Authorization': '{}'.format(token)} + + +def dict_clean(d): + """ Remove None from dictionary """ + if not isinstance(d, dict): + return d + return dict((k, dict_clean(v)) for k, v in d.items() if v is not None) diff --git a/azext_iot/operations/_mqtt.py b/azext_iot/operations/_mqtt.py index 1785446d7..9d36d62a6 100644 --- a/azext_iot/operations/_mqtt.py +++ b/azext_iot/operations/_mqtt.py @@ -8,9 +8,11 @@ import ssl import os -from time import time, sleep import six + +from time import time, sleep from paho.mqtt import client as mqtt + from azext_iot._constants import EXTENSION_ROOT from azext_iot.common.sas_token_auth import SasTokenAuthentication diff --git a/azext_iot/operations/digitaltwin.py b/azext_iot/operations/digitaltwin.py new file mode 100644 index 000000000..d351f64f4 --- /dev/null +++ b/azext_iot/operations/digitaltwin.py @@ -0,0 +1,278 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Unpublished works. +# -------------------------------------------------------------------------------------------- + +from os.path import exists +from knack.util import CLIError +from azure.cli.core.util import read_file_content +from azext_iot._constants import PNP_ENDPOINT +from azext_iot._factory import _bind_sdk +from azext_iot.common.shared import SdkType, ModelSourceType +from azext_iot.common._azure import get_iot_hub_connection_string +from azext_iot.common.utility import (shell_safe_json_parse, + unpack_msrest_error) +from azext_iot.operations.pnp import (iot_pnp_interface_show, + iot_pnp_interface_list, + _validate_repository) +from azext_iot.operations.hub import _iot_hub_monitor_events + + +INTERFACE_KEY_NAME = 'urn_azureiot_ModelDiscovery_DigitalTwin' +INTERFACE_COMMAND = 'Command' +INTERFACE_PROPERTY = 'Property' +INTERFACE_TELEMETRY = 'Telemetry' +INTERFACE_MODELDEFINITION = 'urn_azureiot_ModelDiscovery_ModelDefinition' +INTERFACE_COMMANDNAME = 'getModelDefinition' + + +def iot_digitaltwin_interface_list(cmd, device_id, hub_name=None, resource_group_name=None, login=None): + device_default_interface = _iot_digitaltwin_interface_show(cmd, device_id, INTERFACE_KEY_NAME, + hub_name, resource_group_name, login) + result = _get_device_default_interface_dict(device_default_interface) + return {'interfaces': result} + + +def iot_digitaltwin_command_list(cmd, device_id, source_model, interface=None, schema=False, + repo_endpoint=PNP_ENDPOINT, repo_id=None, repo_login=None, + hub_name=None, resource_group_name=None, login=None): + result = [] + target_interfaces = [] + source_model = source_model.lower() + device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login) + interface_list = _get_device_default_interface_dict(device_interfaces) + target_interface = next((item for item in interface_list if item['name'] == interface), None) + if interface and not target_interface: + raise CLIError('Target interface is not implemented by the device!') + + if interface: + target_interfaces.append(target_interface) + else: + target_interfaces = interface_list + + for entity in target_interfaces: + interface_result = {'name': entity['name'], 'urn_id': entity['urn_id'], 'commands': {}} + interface_commands = [] + found_commands = [] + if source_model == ModelSourceType.device.value.lower(): + found_commands = _device_interface_elements(cmd, device_id, entity['urn_id'], INTERFACE_COMMAND, + hub_name, resource_group_name, login) + else: + if source_model == ModelSourceType.private.value.lower(): + _validate_repository(repo_id, repo_login) + found_commands = _pnp_interface_elements(cmd, entity['urn_id'], INTERFACE_COMMAND, + repo_endpoint, repo_id, repo_login) + for command in found_commands: + command.pop('@type', None) + if schema: + interface_commands.append(command) + else: + interface_commands.append(command.get('name')) + interface_result['commands'] = interface_commands + result.append(interface_result) + return {'interfaces': result} + + +def iot_digitaltwin_properties_list(cmd, device_id, source_model, interface=None, schema=False, + repo_endpoint=PNP_ENDPOINT, repo_id=None, repo_login=None, + hub_name=None, resource_group_name=None, login=None): + result = [] + target_interfaces = [] + source_model = source_model.lower() + device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login) + interface_list = _get_device_default_interface_dict(device_interfaces) + target_interface = next((item for item in interface_list if item['name'] == interface), None) + if interface and not target_interface: + raise CLIError('Target interface is not implemented by the device!') + + if interface: + target_interfaces.append(target_interface) + else: + target_interfaces = interface_list + + for entity in target_interfaces: + interface_result = {'name': entity['name'], 'urn_id': entity['urn_id'], 'properties': {}} + interface_properties = [] + found_properties = [] + if source_model == ModelSourceType.device.value.lower(): + found_properties = _device_interface_elements(cmd, device_id, entity['urn_id'], INTERFACE_PROPERTY, + hub_name, resource_group_name, login) + else: + if source_model == ModelSourceType.private.value.lower(): + _validate_repository(repo_id, repo_login) + found_properties = _pnp_interface_elements(cmd, entity['urn_id'], INTERFACE_PROPERTY, + repo_endpoint, repo_id, repo_login) + for prop in found_properties: + prop.pop('@type', None) + if schema: + interface_properties.append(prop) + else: + interface_properties.append(prop.get('name')) + interface_result['properties'] = interface_properties + result.append(interface_result) + return {'interfaces': result} + + +# pylint: disable=too-many-locals +def iot_digitaltwin_invoke_command(cmd, interface, device_id, command_name, command_payload=None, + timeout=10, hub_name=None, resource_group_name=None, login=None): + device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login) + interface_list = _get_device_default_interface_dict(device_interfaces) + + target_interface = next((item for item in interface_list if item['name'] == interface), None) + + if not target_interface: + raise CLIError('Target interface is not implemented by the device!') + + if command_payload: + if exists(command_payload): + command_payload = str(read_file_content(command_payload)) + + target_json = None + try: + target_json = shell_safe_json_parse(command_payload) + except ValueError: + pass + + if target_json or isinstance(target_json, bool): + command_payload = target_json + + target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) + try: + result = service_sdk.invoke_interface_command(device_id, + interface, + command_name, + command_payload, + connect_timeout_in_seconds=timeout, + response_timeout_in_seconds=timeout) + return result + except errors.CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + +def iot_digitaltwin_property_update(cmd, interface_payload, device_id, + hub_name=None, resource_group_name=None, login=None): + if exists(interface_payload): + interface_payload = str(read_file_content(interface_payload)) + + target_json = None + try: + target_json = shell_safe_json_parse(interface_payload) + except ValueError: + pass + + if target_json: + interface_payload = target_json + + target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) + try: + result = service_sdk.update_interfaces(device_id, interfaces=interface_payload) + return result + except errors.CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + +def iot_digitaltwin_monitor_events(cmd, device_id, interface, source_model, repo_endpoint=PNP_ENDPOINT, repo_id=None, + consumer_group='$Default', timeout=300, hub_name=None, resource_group_name=None, + yes=False, properties=None, repair=False, login=None, repo_login=None): + source_model = source_model.lower() + pnp_context = {'enabled': True, 'interface': {}} + device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login) + interface_list = _get_device_default_interface_dict(device_interfaces) + + target_interface = next((k for k in interface_list if k['name'] == interface), None) + if interface and not target_interface: + raise CLIError('Target interface is not implemented by the device!') + + pnp_context['interface'][target_interface['urn_id']] = {} + found_telemetry = [] + if source_model == ModelSourceType.device.value.lower(): + found_telemetry = _device_interface_elements(cmd, device_id, target_interface['urn_id'], INTERFACE_TELEMETRY, + hub_name, resource_group_name, login) + else: + if source_model == ModelSourceType.private.value.lower(): + _validate_repository(repo_id, repo_login) + found_telemetry = _pnp_interface_elements(cmd, target_interface['urn_id'], INTERFACE_TELEMETRY, + repo_endpoint, repo_id, repo_login) + + for telemetry in found_telemetry: + telemetry_data = {'display': telemetry.get('displayName'), 'unit': telemetry.get('unit')} + pnp_context['interface'][target_interface['urn_id']][telemetry['name']] = telemetry_data + + _iot_hub_monitor_events(cmd=cmd, interface=target_interface['urn_id'], pnp_context=pnp_context, + hub_name=hub_name, device_id=device_id, consumer_group=consumer_group, timeout=timeout, + enqueued_time=None, resource_group_name=resource_group_name, + yes=yes, properties=properties, repair=repair, + login=login) + + +def _iot_digitaltwin_interface_show(cmd, device_id, interface, hub_name=None, resource_group_name=None, login=None): + target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) + try: + device_interface = service_sdk.get_interface(device_id, interface) + return device_interface + except errors.CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + +def _iot_digitaltwin_interface_list(cmd, device_id, hub_name=None, resource_group_name=None, login=None): + target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) + try: + device_interfaces = service_sdk.get_interfaces(device_id) + return device_interfaces + except errors.CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + +def _get_device_default_interface_dict(device_default_interface): + interface = device_default_interface['interfaces'][INTERFACE_KEY_NAME] + result = [] + for k, v in interface['properties']['modelInformation']['reported']['value']['interfaces'].items(): + result.append({'name': k, "urn_id": v}) + return result + + +def _pnp_interface_elements(cmd, interface, target_type, repo_endpoint, repo_id, login): + interface_elements = [] + results = iot_pnp_interface_list(cmd, repo_endpoint, repo_id, interface, login=login) + if results: + interface_def = iot_pnp_interface_show(cmd, interface, repo_endpoint, repo_id, login) + interface_contents = interface_def.get('contents') + for content in interface_contents: + if isinstance(content.get('@type'), list) and target_type in content.get('@type'): + interface_elements.append(content) + elif content.get('@type') == target_type: + interface_elements.append(content) + return interface_elements + + +def _device_interface_elements(cmd, device_id, interface, target_type, hub_name, resource_group_name, login): + target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) + interface_elements = [] + try: + payload = {'id': {}} + payload['id'] = interface + target_payload = shell_safe_json_parse(str(payload)) + interface_def = service_sdk.invoke_interface_command(device_id, + INTERFACE_MODELDEFINITION, + INTERFACE_COMMANDNAME, + target_payload) + if interface_def and interface_def.get('contents'): + interface_contents = interface_def.get('contents') + for content in interface_contents: + if isinstance(content.get('@type'), list) and target_type in content.get('@type'): + interface_elements.append(content) + elif content.get('@type') == target_type: + interface_elements.append(content) + return interface_elements + except errors.CloudError as e: + raise CLIError(unpack_msrest_error(e)) + except Exception: # pylint: disable=broad-except + # returning an empty collection to continue + return [] diff --git a/azext_iot/operations/dps.py b/azext_iot/operations/dps.py index 783a7b42b..0d800df5d 100644 --- a/azext_iot/operations/dps.py +++ b/azext_iot/operations/dps.py @@ -409,15 +409,16 @@ def iot_dps_registration_delete(client, dps_name, resource_group_name, registrat def _get_initial_twin(initial_twin_tags=None, initial_twin_properties=None): + from azext_iot.common.utility import dict_clean if initial_twin_tags == "": initial_twin_tags = None elif initial_twin_tags: - initial_twin_tags = shell_safe_json_parse(str(initial_twin_tags)) + initial_twin_tags = dict_clean(shell_safe_json_parse(str(initial_twin_tags))) if initial_twin_properties == "": initial_twin_properties = None elif initial_twin_properties: - initial_twin_properties = shell_safe_json_parse(str(initial_twin_properties)) + initial_twin_properties = dict_clean(shell_safe_json_parse(str(initial_twin_properties))) return InitialTwin(TwinCollection(initial_twin_tags), InitialTwinProperties(TwinCollection(initial_twin_properties))) diff --git a/azext_iot/operations/events3/_events.py b/azext_iot/operations/events3/_events.py index 0d93eae84..ccce57eea 100644 --- a/azext_iot/operations/events3/_events.py +++ b/azext_iot/operations/events3/_events.py @@ -10,11 +10,11 @@ import sys from time import time from uuid import uuid4 - import six import uamqp import yaml + from azext_iot._constants import VERSION from azext_iot.common.sas_token_auth import SasTokenAuthentication from azext_iot.common.utility import (parse_entity, unicode_binary_map, @@ -26,11 +26,15 @@ DEBUG = True -def executor(target, consumer_group, enqueued_time, device_id=None, properties=None, timeout=0, output=None, content_type=None, - devices=None): +def executor(target, consumer_group, enqueued_time, properties=None, + timeout=0, device_id=None, output=None, content_type=None, + devices=None, interface_id=None, pnp_context=None): + coroutines = [] - coroutines.append(initiate_event_monitor(target, consumer_group, enqueued_time, device_id, properties, - timeout, output, content_type, devices)) + coroutines.append(initiate_event_monitor(target, consumer_group, enqueued_time, + device_id, properties, timeout, output, content_type, devices, + interface_id, pnp_context)) + loop = asyncio.get_event_loop() if loop.is_closed(): loop = asyncio.new_event_loop() @@ -50,7 +54,8 @@ def stop_and_suppress_eloop(): except Exception: # pylint: disable=broad-except pass - six.print_('Starting event monitor,{} use ctrl-c to stop...'.format(device_filter_txt if device_filter_txt else '')) + six.print_('Starting {}event monitor,{} use ctrl-c to stop...'.format('PnP ' if pnp_context else '', + device_filter_txt if device_filter_txt else '')) future.add_done_callback(lambda future: stop_and_suppress_eloop()) result = loop.run_until_complete(future) except KeyboardInterrupt: @@ -66,8 +71,9 @@ def stop_and_suppress_eloop(): raise RuntimeError(error) -async def initiate_event_monitor(target, consumer_group, enqueued_time, device_id=None, properties=None, - timeout=0, output=None, content_type=None, devices=None): +async def initiate_event_monitor(target, consumer_group, enqueued_time, device_id=None, + properties=None, timeout=0, output=None, content_type=None, + devices=None, interface_id=None, pnp_context=None): def _get_conn_props(): properties = {} properties["product"] = "az.cli.iot.extension" @@ -112,20 +118,22 @@ def _get_conn_props(): timeout=timeout, output=output, content_type=content_type, - devices=devices)) + devices=devices, + interface_id=interface_id, + pnp_context=pnp_context)) await asyncio.gather(*coroutines, return_exceptions=True) -# pylint: disable=too-many-statements +# pylint: disable=too-many-statements, too-many-branches async def monitor_events(endpoint, connection, path, auth, partition, consumer_group, enqueuedtimeutc, - properties, device_id=None, timeout=0, output=None, content_type=None, devices=None): + properties, device_id=None, timeout=0, output=None, content_type=None, devices=None, + interface_id=None, pnp_context=None): source = uamqp.address.Source('amqps://{}/{}/ConsumerGroups/{}/Partitions/{}'.format(endpoint, path, consumer_group, partition)) source.set_filter( bytes('amqp.annotation.x-opt-enqueuedtimeutc > ' + str(enqueuedtimeutc), 'utf8')) def _output_msg_kpi(msg): - # TODO: Determine if amqp filters can support boolean operators for multiple conditions origin = str(msg.annotations.get(b'iothub-connection-device-id'), 'utf8') if device_id and device_id != origin: if '*' in device_id or '?' in device_id: @@ -137,6 +145,15 @@ def _output_msg_kpi(msg): if devices and origin not in devices: return + if pnp_context: + msg_interface_id = str(msg.annotations.get(b'iothub-interface-id'), 'utf8') + if not msg_interface_id: + return + + if interface_id: + if msg_interface_id != interface_id: + return + event_source = {'event': {}} event_source['event']['origin'] = origin @@ -162,6 +179,23 @@ def _output_msg_kpi(msg): event_source['event']['payload'] = payload + if pnp_context: + event_source['event']['interface'] = msg_interface_id + + msg_schema = str(msg.application_properties.get(b'iothub-message-schema'), 'utf8') + interface_context = pnp_context['interface'].get(msg_interface_id) + if interface_context: + msg_schema_context = interface_context.get(msg_schema) + if msg_schema_context: + msg_context_display = msg_schema_context.get('display') + msg_context_unit = msg_schema_context.get('unit') + + if msg_context_display: + event_source['event']['payload'] = {} + event_source['event']['payload'][msg_context_display] = payload + if msg_context_unit: + event_source['event']['payload']['unit'] = msg_context_unit + if 'anno' in properties or 'all' in properties: event_source['event']['annotations'] = unicode_binary_map(msg.annotations) if 'sys' in properties or 'all' in properties: diff --git a/azext_iot/operations/generic.py b/azext_iot/operations/generic.py index 87ccd22c8..da171236e 100644 --- a/azext_iot/operations/generic.py +++ b/azext_iot/operations/generic.py @@ -10,7 +10,7 @@ def _execute_query(query, query_method, top=None): payload = [] - headers = {} + headers = {'Cache-Control': 'no-cache, must-revalidate'} if top: headers['x-ms-max-item-count'] = str(top) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 31e47fb09..f9fe8c84b 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -11,7 +11,6 @@ from knack.log import get_logger from knack.util import CLIError from azure.cli.core.util import read_file_content -from azext_iot.common.utility import calculate_millisec_since_unix_epoch_utc from azext_iot._constants import (EXTENSION_ROOT, BASE_API_VERSION, DEVICE_DEVICESCOPE_PREFIX, @@ -24,6 +23,7 @@ MetricType) from azext_iot.common._azure import get_iot_hub_connection_string from azext_iot.common.utility import (shell_safe_json_parse, + calculate_millisec_since_unix_epoch_utc, validate_key_value_pairs, url_encode_dict, evaluate_literal, unpack_msrest_error) from azext_iot._factory import _bind_sdk @@ -1288,6 +1288,38 @@ def iot_device_upload_file(cmd, device_id, file_path, content_type, hub_name=Non def iot_hub_monitor_events(cmd, hub_name=None, device_id=None, consumer_group='$Default', timeout=300, enqueued_time=None, resource_group_name=None, yes=False, properties=None, repair=False, login=None, content_type=None, device_query=None): + _iot_hub_monitor_events(cmd, interface=None, pnp_context=None, hub_name=hub_name, device_id=device_id, + consumer_group=consumer_group, timeout=timeout, enqueued_time=enqueued_time, + resource_group_name=resource_group_name, yes=yes, properties=properties, + repair=repair, login=login, content_type=content_type, device_query=device_query) + + +def iot_hub_monitor_feedback(cmd, hub_name=None, device_id=None, yes=False, + wait_on_id=None, repair=False, resource_group_name=None, login=None): + from azext_iot.common.deps import ensure_uamqp + from azext_iot.common.utility import validate_min_python_version + + validate_min_python_version(3, 4) + + config = cmd.cli_ctx.config + ensure_uamqp(config, yes, repair) + + target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + + return _iot_hub_monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id) + + +def iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name=None): + device_twin = _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name) + return _customize_device_tracing_output(device_twin['deviceId'], device_twin['properties']['desired'], + device_twin['properties']['reported']) + + +# pylint: disable=too-many-locals +def _iot_hub_monitor_events(cmd, interface=None, pnp_context=None, + hub_name=None, device_id=None, consumer_group='$Default', timeout=300, + enqueued_time=None, resource_group_name=None, yes=False, properties=None, repair=False, + login=None, content_type=None, device_query=None): import importlib from azext_iot.common.deps import ensure_uamqp from azext_iot.common.utility import validate_min_python_version @@ -1329,35 +1361,9 @@ def iot_hub_monitor_events(cmd, hub_name=None, device_id=None, consumer_group='$ device_id=device_id, output=output, content_type=content_type, - devices=device_ids) - - -def iot_hub_monitor_feedback(cmd, hub_name=None, device_id=None, yes=False, - wait_on_id=None, repair=False, resource_group_name=None, login=None): - from azext_iot.common.deps import ensure_uamqp - from azext_iot.common.utility import validate_min_python_version - - validate_min_python_version(3, 4) - - config = cmd.cli_ctx.config - ensure_uamqp(config, yes, repair) - - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) - - return _iot_hub_monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id) - - -def _iot_hub_monitor_feedback(target, device_id, wait_on_id): - import importlib - - events3 = importlib.import_module('azext_iot.operations.events3._events') - events3.monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id, token_duration=3600) - - -def iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name=None): - device_twin = _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name) - return _customize_device_tracing_output(device_twin['deviceId'], device_twin['properties']['desired'], - device_twin['properties']['reported']) + devices=device_ids, + interface_id=interface, + pnp_context=pnp_context) def iot_hub_distributed_tracing_update(cmd, hub_name, device_id, sampling_mode, sampling_rate, @@ -1374,6 +1380,13 @@ def iot_hub_distributed_tracing_update(cmd, hub_name, device_id, sampling_mode, result.properties.reported) +def _iot_hub_monitor_feedback(target, device_id, wait_on_id): + import importlib + + events3 = importlib.import_module('azext_iot.operations.events3._events') + events3.monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id, token_duration=3600) + + def _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name=None): target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name) device_twin = iot_device_twin_show(cmd, device_id, hub_name, resource_group_name) diff --git a/azext_iot/operations/pnp.py b/azext_iot/operations/pnp.py new file mode 100644 index 000000000..e882d2cb7 --- /dev/null +++ b/azext_iot/operations/pnp.py @@ -0,0 +1,240 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Unpublished works. +# -------------------------------------------------------------------------------------------- + +import json +from os.path import exists +from knack.log import get_logger +from knack.util import CLIError +from azext_iot.common.shared import SdkType, PnPModelType +from azext_iot.common._azure import get_iot_pnp_connection_string +from azext_iot.pnp_sdk.models import SearchOptions +from azext_iot._factory import _bind_sdk +from azext_iot._constants import PNP_API_VERSION, PNP_ENDPOINT +from azext_iot.common.utility import (unpack_pnp_http_error, + get_sas_token, + shell_safe_json_parse) +from azure.cli.core.util import read_file_content + +logger = get_logger(__name__) + + +def iot_pnp_interface_publish(cmd, interface, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): + _validate_repository(repo_id, login) + model_list = _iot_pnp_model_list(cmd, repo_endpoint, repo_id, interface, PnPModelType.interface, + -1, login=login) + if model_list and model_list[0].urn_id == interface: + etag = model_list[0].etag + else: + raise CLIError('No PnP Model definition found for @id "{}"'.format(interface)) + + target_interface = _iot_pnp_model_show(cmd, repo_endpoint, repo_id, + interface, False, PnPModelType.interface, login=login) + + return _iot_pnp_model_publish(cmd, repo_endpoint, repo_id, interface, target_interface, + etag, login=login) + + +def iot_pnp_interface_create(cmd, interface_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): + _validate_repository(repo_id, login) + return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, interface_definition, + PnPModelType.interface, False, login=login) + + +def iot_pnp_interface_update(cmd, interface_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): + _validate_repository(repo_id, login) + return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, interface_definition, + PnPModelType.interface, True, login=login) + + +def iot_pnp_interface_show(cmd, interface, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): + return _iot_pnp_model_show(cmd, repo_endpoint, repo_id, + interface, False, PnPModelType.interface, login=login) + + +def iot_pnp_interface_list(cmd, repo_endpoint=PNP_ENDPOINT, repo_id=None, search_string=None, + top=1000, login=None): + return _iot_pnp_model_list(cmd, repo_endpoint, repo_id, + search_string, PnPModelType.interface, + top, login=login) + + +def iot_pnp_interface_delete(cmd, interface, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): + _validate_repository(repo_id, login) + return _iot_pnp_model_delete(cmd, repo_endpoint, repo_id, interface, login) + + +def iot_pnp_model_publish(cmd, model, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): + _validate_repository(repo_id, login) + model_list = _iot_pnp_model_list(cmd, repo_endpoint, repo_id, model, PnPModelType.capabilityModel, + -1, login=login) + if model_list and model_list[0].urn_id == model: + etag = model_list[0].etag + else: + raise CLIError('No PnP Model definition found for @id "{}"'.format(model)) + + target_model = _iot_pnp_model_show(cmd, repo_endpoint, repo_id, + model, False, PnPModelType.capabilityModel, login=login) + return _iot_pnp_model_publish(cmd, repo_endpoint, repo_id, model, target_model, + etag, login=login) + + +def iot_pnp_model_create(cmd, model_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): + _validate_repository(repo_id, login) + return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, model_definition, + PnPModelType.capabilityModel, False, login=login) + + +def iot_pnp_model_update(cmd, model_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): + _validate_repository(repo_id, login) + return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, model_definition, + PnPModelType.capabilityModel, True, login=login) + + +def iot_pnp_model_show(cmd, model, repo_endpoint=PNP_ENDPOINT, repo_id=None, expand=False, login=None): + return _iot_pnp_model_show(cmd, repo_endpoint, repo_id, + model, expand, PnPModelType.capabilityModel, login=login) + + +def iot_pnp_model_list(cmd, repo_endpoint=PNP_ENDPOINT, repo_id=None, search_string=None, + top=1000, login=None): + return _iot_pnp_model_list(cmd, repo_endpoint, repo_id, + search_string, PnPModelType.capabilityModel, + top, login=login) + + +def iot_pnp_model_delete(cmd, model, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): + _validate_repository(repo_id, login) + return _iot_pnp_model_delete(cmd, repo_endpoint, repo_id, model, login) + + +def _iot_pnp_model_publish(cmd, endpoint, repository, model_id, model_def, etag, login): + + target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) + pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) + + contents = json.loads(json.dumps(model_def, separators=(',', ':'), indent=2)) + try: + headers = get_sas_token(target) + return pnp_sdk.create_or_update_model(model_id, + api_version=PNP_API_VERSION, + content=contents, + if_match=etag, + custom_headers=headers) + except errors.HttpOperationError as e: + raise CLIError(unpack_pnp_http_error(e)) + + +def _iot_pnp_model_create_or_update(cmd, endpoint, repository, model_def, pnpModelType, is_update, login): + + target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) + pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) + etag = None + model_def = _validate_model_definition(model_def) + model_id = model_def.get('@id') + if not model_id: + raise CLIError('PnP Model definition requires @id! Please include @id and try again.') + + if is_update: + model_list = _iot_pnp_model_list(cmd, endpoint, repository, model_id, pnpModelType, + -1, login=login) + if model_list and model_list[0].urn_id == model_id: + etag = model_list[0].etag + else: + raise CLIError('No PnP Model definition found for @id "{}"'.format(model_id)) + + contents = json.loads(json.dumps(model_def, separators=(',', ':'), indent=2)) + try: + headers = get_sas_token(target) + return pnp_sdk.create_or_update_model(model_id, + api_version=PNP_API_VERSION, + content=contents, + repository_id=target.get('repository_id', None), + if_match=etag, + custom_headers=headers) + except errors.HttpOperationError as e: + raise CLIError(unpack_pnp_http_error(e)) + + +def _iot_pnp_model_show(cmd, endpoint, repository, model_id, expand, pnpModelType, login): + target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) + pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) + try: + headers = get_sas_token(target) + result = pnp_sdk.get_model(model_id, api_version=PNP_API_VERSION, + repository_id=target.get('repository_id', None), + custom_headers=headers, + expand=expand) + + if not result or result["@type"].lower() != pnpModelType.value.lower(): + raise CLIError('PnP Model definition for "{}", not found.'.format(model_id)) + + return result + except errors.HttpOperationError as e: + raise CLIError(unpack_pnp_http_error(e)) + + +def _iot_pnp_model_list(cmd, endpoint, repository, search_string, + pnpModelType, top, login): + target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) + + pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) + try: + headers = get_sas_token(target) + search_options = SearchOptions(search_keyword=search_string, + model_filter_type=pnpModelType.value) + if top > 0: + search_options.page_size = top + + result = pnp_sdk.search(search_options, api_version=PNP_API_VERSION, + repository_id=target.get('repository_id', None), + custom_headers=headers) + return result.results + except errors.HttpOperationError as e: + raise CLIError(unpack_pnp_http_error(e)) + + +def _iot_pnp_model_delete(cmd, endpoint, repository, model_id, login): + target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) + + pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) + try: + headers = get_sas_token(target) + return pnp_sdk.delete_model(model_id, + repository_id=target.get('repository_id', None), + api_version=PNP_API_VERSION, + custom_headers=headers) + except errors.HttpOperationError as e: + raise CLIError(unpack_pnp_http_error(e)) + + +def _looks_like_file(element): + element = element.lower() + if element.endswith(('.txt', '.json', '.md', '.rst', '.doc', '.docx')): + return True + return False + + +def _validate_model_definition(model_def): + if exists(model_def): + model_def = str(read_file_content(model_def)) + else: + logger.info('Definition not from file path or incorrect path given.') + + try: + return shell_safe_json_parse(model_def) + except ValueError as e: + logger.debug('Received definition: %s', model_def) + if _looks_like_file(model_def): + raise CLIError('The definition content looks like its from a file. Please ensure the path is correct.') + raise CLIError('Malformed capability model definition. ' + 'Use --debug to see what was received. Error details: {}'.format(e)) + + +def _validate_repository(repo_id, login): + if not login and not repo_id: + raise CLIError('Please provide the model repository\'s repositoryId (via the \'--repo-id\' or \'-r\' parameter)' + ' and endpoint (via the \'--endpoint\' or \'-e\' parameter) or model repository\'s connection' + ' string via --login...') diff --git a/azext_iot/pnp_sdk/__init__.py b/azext_iot/pnp_sdk/__init__.py new file mode 100644 index 000000000..b77741974 --- /dev/null +++ b/azext_iot/pnp_sdk/__init__.py @@ -0,0 +1,18 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twin_repository_service import DigitalTwinRepositoryService +from .version import VERSION + +__all__ = ['DigitalTwinRepositoryService'] + +__version__ = VERSION + diff --git a/azext_iot/pnp_sdk/digital_twin_repository_service.py b/azext_iot/pnp_sdk/digital_twin_repository_service.py new file mode 100644 index 000000000..b1250f91a --- /dev/null +++ b/azext_iot/pnp_sdk/digital_twin_repository_service.py @@ -0,0 +1,360 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.service_client import SDKClient +from msrest import Configuration, Serializer, Deserializer +from .version import VERSION +from msrest.pipeline import ClientRawResponse +from msrest.exceptions import HttpOperationError +from . import models + + +class DigitalTwinRepositoryServiceConfiguration(Configuration): + """Configuration for DigitalTwinRepositoryService + Note that all parameters used to create this instance are saved as instance + attributes. + + :param str base_url: Service URL + """ + + def __init__( + self, base_url=None): + + if not base_url: + base_url = 'http://localhost' + + super(DigitalTwinRepositoryServiceConfiguration, self).__init__(base_url) + + self.add_user_agent('digitaltwinrepositoryservice/{}'.format(VERSION)) + + +class DigitalTwinRepositoryService(SDKClient): + """DigitalTwin Model Repository Service. + + :ivar config: Configuration for client. + :vartype config: DigitalTwinRepositoryServiceConfiguration + + :param str base_url: Service URL + """ + + def __init__( + self, base_url=None): + + self.config = DigitalTwinRepositoryServiceConfiguration(base_url) + super(DigitalTwinRepositoryService, self).__init__(None, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = 'v1' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + + def get_model( + self, model_id, api_version, repository_id=None, x_ms_client_request_id=None, expand=False, custom_headers=None, raw=False, **operation_config): + """Returns a DigitalTwin model object for the given \"id\".\r\nIf + \"expand\" is present in the query parameters and \"id\" is for a + capability model then it returns\r\nthe capability model with expanded + interface definitions. + + :param model_id: Model id Ex: + urn:contoso:com:temparaturesensor:1 + :type model_id: str + :param api_version: Version of the Api. Must be 2019-07-01-preview + :type api_version: str + :param repository_id: To access private repo, repositoryId is the repo + id. To access global repo, caller should not specify this value. + :type repository_id: str + :param x_ms_client_request_id: Optional. Provides a client-generated + opaque value that is recorded in the logs. Using this header is highly + recommended for correlating client-side activities with requests + received by the server. + :type x_ms_client_request_id: str + :param expand: Indicates whether to expand the capability model's + interface definitions inline or not. This query parameter ONLY applies + to Capability model. + :type expand: bool + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`HttpOperationError` + """ + # Construct URL + url = self.get_model.metadata['url'] + path_format_arguments = { + 'modelId': self._serialize.url("model_id", model_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + if repository_id is not None: + query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str') + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + if expand is not None: + query_parameters['expand'] = self._serialize.query("expand", expand, 'bool') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/ld+json' + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise HttpOperationError(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('{object}', response) + header_dict = { + 'x-ms-request-id': 'str', + 'ETag': 'str', + 'x-ms-model-id': 'str', + 'x-ms-model-publisher-id': 'str', + 'x-ms-model-publisher-name': 'str', + 'x-ms-model-createdon': 'iso-8601', + 'x-ms-model-lastupdated': 'iso-8601', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + get_model.metadata = {'url': '/models/{modelId}'} + + def create_or_update_model( + self, model_id, api_version, content, repository_id=None, x_ms_client_request_id=None, if_match=None, custom_headers=None, raw=False, **operation_config): + """Creates or updates the DigitalTwin Model in the repository. + + :param model_id: Model id Ex: + urn:contoso:TemparatureSensor:1 + :type model_id: str + :param api_version: Version of the Api. Must be 2019-07-01-preview + :type api_version: str + :param content: Model definition in Digital Twin Definition Language + format. + :type content: object + :param repository_id: To access private repo, repositoryId is the repo + id\\r\\nTo access global repo, caller should not specify this value. + :type repository_id: str + :param x_ms_client_request_id: Optional. Provides a client-generated + opaque value that is recorded in the logs. Using this header is highly + recommended for correlating client-side activities with requests + received by the server. + :type x_ms_client_request_id: str + :param if_match: Used to make operation conditional for optimistic + concurrency. That is, the document is updated only if the specified + etag matches the current version in the database. The value should be + set to the etag value of the resource. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`HttpOperationError` + """ + # Construct URL + url = self.create_or_update_model.metadata['url'] + path_format_arguments = { + 'modelId': self._serialize.url("model_id", model_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + if repository_id is not None: + query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str') + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + + # Construct body + body_content = self._serialize.body(content, 'object') + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [201, 204, 412]: + raise HttpOperationError(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + client_raw_response.add_headers({ + 'x-ms-request-id': 'str', + 'ETag': 'str', + }) + return client_raw_response + create_or_update_model.metadata = {'url': '/models/{modelId}'} + + def delete_model( + self, model_id, repository_id, api_version, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Deletes a digital twin model from the repository. + + :param model_id: Model id Ex: + urn:contoso:com:temparaturesensor:1 + :type model_id: str + :param repository_id: To access private repo, repositoryId is the repo + id. Delete is not allowed for public repository. + :type repository_id: str + :param api_version: Version of the Api. Must be 2019-07-01-preview + :type api_version: str + :param x_ms_client_request_id: Optional. Provides a client-generated + opaque value that is recorded in the logs. Using this header is highly + recommended for correlating client-side activities with requests + received by the server. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`HttpOperationError` + """ + # Construct URL + url = self.delete_model.metadata['url'] + path_format_arguments = { + 'modelId': self._serialize.url("model_id", model_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str') + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise HttpOperationError(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + client_raw_response.add_headers({ + 'x-ms-request-id': 'str', + }) + return client_raw_response + delete_model.metadata = {'url': '/models/{modelId}'} + + def search( + self, search_options, api_version, repository_id=None, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Searches pnp models for given search options. + It searches in the "Description, DisplayName, Comment and Id" metadata. + + :param search_options: Set SearchOption.searchKeyword to search models + with the keyword. + Set the "SearchOptions.modelFilterType" to restrict to a type of + DigitalTwin model (Ex: Interface or CapabilityModel). + Default it returns all the models. + :type search_options: + ~digitaltwinmodelrepositoryservice.models.SearchOptions + :param api_version: Version of the Api. Must be 2019-07-01-preview + :type api_version: str + :param repository_id: To access private repo, repositoryId is the repo + id.\\r\\nDelete is not allowed for public repository. + :type repository_id: str + :param x_ms_client_request_id: Optional. Provides a client-generated + opaque value that is recorded in the logs. Using this header is highly + recommended for correlating client-side activities with requests + received by the server.. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: SearchResponse or ClientRawResponse if raw=true + :rtype: ~digitaltwinmodelrepositoryservice.models.SearchResponse or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`HttpOperationError` + """ + # Construct URL + url = self.search.metadata['url'] + + # Construct parameters + query_parameters = {} + if repository_id is not None: + query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str') + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + + # Construct body + body_content = self._serialize.body(search_options, 'SearchOptions') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise HttpOperationError(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('SearchResponse', response) + header_dict = { + 'x-ms-request-id': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + search.metadata = {'url': '/models/search'} diff --git a/azext_iot/pnp_sdk/models/__init__.py b/azext_iot/pnp_sdk/models/__init__.py new file mode 100644 index 000000000..21a611417 --- /dev/null +++ b/azext_iot/pnp_sdk/models/__init__.py @@ -0,0 +1,25 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +try: + from .search_options_py3 import SearchOptions + from .model_information_py3 import ModelInformation + from .search_response_py3 import SearchResponse +except (SyntaxError, ImportError): + from .search_options import SearchOptions + from .model_information import ModelInformation + from .search_response import SearchResponse + +__all__ = [ + 'SearchOptions', + 'ModelInformation', + 'SearchResponse', +] diff --git a/azext_iot/pnp_sdk/models/model_information.py b/azext_iot/pnp_sdk/models/model_information.py new file mode 100644 index 000000000..c3f804945 --- /dev/null +++ b/azext_iot/pnp_sdk/models/model_information.py @@ -0,0 +1,72 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelInformation(Model): + """ModelInformation. + + :param comment: + :type comment: str + :param description: + :type description: str + :param display_name: + :type display_name: str + :param urn_id: + :type urn_id: str + :param model_name: + :type model_name: str + :param version: + :type version: int + :param type: Possible values include: 'interface', 'capabilityModel' + :type type: str or ~digitaltwinmodelrepositoryservice.models.enum + :param etag: + :type etag: str + :param publisher_id: + :type publisher_id: str + :param publisher_name: + :type publisher_name: str + :param created_on: + :type created_on: datetime + :param updated_on: + :type updated_on: datetime + """ + + _attribute_map = { + 'comment': {'key': 'comment', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': 'str'}, + 'urn_id': {'key': 'urnId', 'type': 'str'}, + 'model_name': {'key': 'modelName', 'type': 'str'}, + 'version': {'key': 'version', 'type': 'int'}, + 'type': {'key': 'type', 'type': 'str'}, + 'etag': {'key': 'etag', 'type': 'str'}, + 'publisher_id': {'key': 'publisherId', 'type': 'str'}, + 'publisher_name': {'key': 'publisherName', 'type': 'str'}, + 'created_on': {'key': 'createdOn', 'type': 'iso-8601'}, + 'updated_on': {'key': 'updatedOn', 'type': 'iso-8601'}, + } + + def __init__(self, **kwargs): + super(ModelInformation, self).__init__(**kwargs) + self.comment = kwargs.get('comment', None) + self.description = kwargs.get('description', None) + self.display_name = kwargs.get('display_name', None) + self.urn_id = kwargs.get('urn_id', None) + self.model_name = kwargs.get('model_name', None) + self.version = kwargs.get('version', None) + self.type = kwargs.get('type', None) + self.etag = kwargs.get('etag', None) + self.publisher_id = kwargs.get('publisher_id', None) + self.publisher_name = kwargs.get('publisher_name', None) + self.created_on = kwargs.get('created_on', None) + self.updated_on = kwargs.get('updated_on', None) diff --git a/azext_iot/pnp_sdk/models/model_information_py3.py b/azext_iot/pnp_sdk/models/model_information_py3.py new file mode 100644 index 000000000..4ec8acd27 --- /dev/null +++ b/azext_iot/pnp_sdk/models/model_information_py3.py @@ -0,0 +1,72 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelInformation(Model): + """ModelInformation. + + :param comment: + :type comment: str + :param description: + :type description: str + :param display_name: + :type display_name: str + :param urn_id: + :type urn_id: str + :param model_name: + :type model_name: str + :param version: + :type version: int + :param type: Possible values include: 'interface', 'capabilityModel' + :type type: str or ~digitaltwinmodelrepositoryservice.models.enum + :param etag: + :type etag: str + :param publisher_id: + :type publisher_id: str + :param publisher_name: + :type publisher_name: str + :param created_on: + :type created_on: datetime + :param updated_on: + :type updated_on: datetime + """ + + _attribute_map = { + 'comment': {'key': 'comment', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': 'str'}, + 'urn_id': {'key': 'urnId', 'type': 'str'}, + 'model_name': {'key': 'modelName', 'type': 'str'}, + 'version': {'key': 'version', 'type': 'int'}, + 'type': {'key': 'type', 'type': 'str'}, + 'etag': {'key': 'etag', 'type': 'str'}, + 'publisher_id': {'key': 'publisherId', 'type': 'str'}, + 'publisher_name': {'key': 'publisherName', 'type': 'str'}, + 'created_on': {'key': 'createdOn', 'type': 'iso-8601'}, + 'updated_on': {'key': 'updatedOn', 'type': 'iso-8601'}, + } + + def __init__(self, *, comment: str=None, description: str=None, display_name: str=None, urn_id: str=None, model_name: str=None, version: int=None, type=None, etag: str=None, publisher_id: str=None, publisher_name: str=None, created_on=None, updated_on=None, **kwargs) -> None: + super(ModelInformation, self).__init__(**kwargs) + self.comment = comment + self.description = description + self.display_name = display_name + self.urn_id = urn_id + self.model_name = model_name + self.version = version + self.type = type + self.etag = etag + self.publisher_id = publisher_id + self.publisher_name = publisher_name + self.created_on = created_on + self.updated_on = updated_on diff --git a/azext_iot/pnp_sdk/models/search_options.py b/azext_iot/pnp_sdk/models/search_options.py new file mode 100644 index 000000000..497740e04 --- /dev/null +++ b/azext_iot/pnp_sdk/models/search_options.py @@ -0,0 +1,42 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SearchOptions(Model): + """SearchOptions. + + :param search_keyword: + :type search_keyword: str + :param model_filter_type: Possible values include: 'interface', + 'capabilityModel' + :type model_filter_type: str or + ~digitaltwinmodelrepositoryservice.models.enum + :param continuation_token: + :type continuation_token: str + :param page_size: + :type page_size: int + """ + + _attribute_map = { + 'search_keyword': {'key': 'searchKeyword', 'type': 'str'}, + 'model_filter_type': {'key': 'modelFilterType', 'type': 'str'}, + 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, + 'page_size': {'key': 'pageSize', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(SearchOptions, self).__init__(**kwargs) + self.search_keyword = kwargs.get('search_keyword', None) + self.model_filter_type = kwargs.get('model_filter_type', None) + self.continuation_token = kwargs.get('continuation_token', None) + self.page_size = kwargs.get('page_size', None) diff --git a/azext_iot/pnp_sdk/models/search_options_py3.py b/azext_iot/pnp_sdk/models/search_options_py3.py new file mode 100644 index 000000000..f6da4fd89 --- /dev/null +++ b/azext_iot/pnp_sdk/models/search_options_py3.py @@ -0,0 +1,42 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SearchOptions(Model): + """SearchOptions. + + :param search_keyword: + :type search_keyword: str + :param model_filter_type: Possible values include: 'interface', + 'capabilityModel' + :type model_filter_type: str or + ~digitaltwinmodelrepositoryservice.models.enum + :param continuation_token: + :type continuation_token: str + :param page_size: + :type page_size: int + """ + + _attribute_map = { + 'search_keyword': {'key': 'searchKeyword', 'type': 'str'}, + 'model_filter_type': {'key': 'modelFilterType', 'type': 'str'}, + 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, + 'page_size': {'key': 'pageSize', 'type': 'int'}, + } + + def __init__(self, *, search_keyword: str=None, model_filter_type=None, continuation_token: str=None, page_size: int=None, **kwargs) -> None: + super(SearchOptions, self).__init__(**kwargs) + self.search_keyword = search_keyword + self.model_filter_type = model_filter_type + self.continuation_token = continuation_token + self.page_size = page_size diff --git a/azext_iot/pnp_sdk/models/search_response.py b/azext_iot/pnp_sdk/models/search_response.py new file mode 100644 index 000000000..a6e36d521 --- /dev/null +++ b/azext_iot/pnp_sdk/models/search_response.py @@ -0,0 +1,33 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SearchResponse(Model): + """SearchResponse. + + :param continuation_token: + :type continuation_token: str + :param results: + :type results: + list[~digitaltwinmodelrepositoryservice.models.ModelInformation] + """ + + _attribute_map = { + 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, + 'results': {'key': 'results', 'type': '[ModelInformation]'}, + } + + def __init__(self, **kwargs): + super(SearchResponse, self).__init__(**kwargs) + self.continuation_token = kwargs.get('continuation_token', None) + self.results = kwargs.get('results', None) diff --git a/azext_iot/pnp_sdk/models/search_response_py3.py b/azext_iot/pnp_sdk/models/search_response_py3.py new file mode 100644 index 000000000..94d05c147 --- /dev/null +++ b/azext_iot/pnp_sdk/models/search_response_py3.py @@ -0,0 +1,33 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SearchResponse(Model): + """SearchResponse. + + :param continuation_token: + :type continuation_token: str + :param results: + :type results: + list[~digitaltwinmodelrepositoryservice.models.ModelInformation] + """ + + _attribute_map = { + 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, + 'results': {'key': 'results', 'type': '[ModelInformation]'}, + } + + def __init__(self, *, continuation_token: str=None, results=None, **kwargs) -> None: + super(SearchResponse, self).__init__(**kwargs) + self.continuation_token = continuation_token + self.results = results diff --git a/azext_iot/pnp_sdk/version.py b/azext_iot/pnp_sdk/version.py new file mode 100644 index 000000000..8bf1b66a3 --- /dev/null +++ b/azext_iot/pnp_sdk/version.py @@ -0,0 +1,13 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +VERSION = "v1" + diff --git a/azext_iot/service_sdk/iot_hub_gateway_service_apis.py b/azext_iot/service_sdk/iot_hub_gateway_service_apis.py index d7faaa391..1be0cdb83 100644 --- a/azext_iot/service_sdk/iot_hub_gateway_service_apis.py +++ b/azext_iot/service_sdk/iot_hub_gateway_service_apis.py @@ -18,7 +18,7 @@ import uuid from . import models from azext_iot._constants import VERSION as extver -from azext_iot._constants import BASE_API_VERSION +from azext_iot._constants import BASE_API_VERSION, PNP_API_VERSION class IotHubGatewayServiceAPIsConfiguration(AzureConfiguration): @@ -2357,3 +2357,376 @@ def invoke_device_method1( # @digimaun - change device param from {id} to {deviceId} invoke_device_method1.metadata = {'url': '/twins/{deviceId}/modules/{moduleId}/methods'} + + def get_interfaces( + self, digital_twin_id, custom_headers=None, raw=False, **operation_config): + """Gets the list of interfaces. + + :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is + DeviceId[~ModuleId]. ModuleId is optional. + :type digital_twin_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: DigitalTwinInterfaces or ClientRawResponse if raw=true + :rtype: ~service.models.DigitalTwinInterfaces or + ~msrest.pipeline.ClientRawResponse + :raises: class:`CloudError` + """ + # Construct URL + url = self.get_interfaces.metadata['url'] + path_format_arguments = { + 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released + query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters) + response = self._client.send(request, header_parameters, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + header_dict = {} + + # @anusapan - deserialize as {object} from DigitalTwinInterfaces + if response.status_code == 200: + deserialized = self._deserialize('{object}', response) + header_dict = { + 'ETag': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + get_interfaces.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces'} + + def update_interfaces( + self, digital_twin_id, if_match=None, interfaces=None, custom_headers=None, raw=False, **operation_config): + """Updates desired properties of multiple interfaces. + Example URI: "digitalTwins/{digitalTwinId}/interfaces". + + :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is + DeviceId[~ModuleId]. ModuleId is optional. + :type digital_twin_id: str + :param if_match: + :type if_match: str + :param interfaces: Interface(s) data to patch in the digital twin. + :type interfaces: dict[str, + ~service.models.DigitalTwinInterfacesPatchInterfacesValue] + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: DigitalTwinInterfaces or ClientRawResponse if raw=true + :rtype: ~service.models.DigitalTwinInterfaces or + ~msrest.pipeline.ClientRawResponse + :raises: class:`CloudError` + """ + interfaces_patch_info = models.DigitalTwinInterfacesPatch(interfaces=interfaces) + + # Construct URL + url = self.update_interfaces.metadata['url'] + path_format_arguments = { + 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released + query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + + # Construct body + body_content = self._serialize.body(interfaces_patch_info, 'DigitalTwinInterfacesPatch') + + # Construct and send request + request = self._client.patch(url, query_parameters) + response = self._client.send(request, header_parameters, body_content, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + + deserialized = None + header_dict = {} + + # @anusapan - deserialize as {object} from DigitalTwinInterfaces + if response.status_code == 200: + deserialized = self._deserialize('{object}', response) + header_dict = { + 'ETag': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + update_interfaces.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces'} + + def get_interface( + self, digital_twin_id, interface_name, custom_headers=None, raw=False, **operation_config): + """Gets the interface of given interfaceId. + Example URI: "digitalTwins/{digitalTwinId}/interfaces/{interfaceName}". + + :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is + DeviceId[~ModuleId]. ModuleId is optional. + :type digital_twin_id: str + :param interface_name: The interface name. + :type interface_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: DigitalTwinInterfaces or ClientRawResponse if raw=true + :rtype: ~service.models.DigitalTwinInterfaces or + ~msrest.pipeline.ClientRawResponse + :raises: class:`CloudError` + """ + # Construct URL + url = self.get_interface.metadata['url'] + path_format_arguments = { + 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str'), + 'interfaceName': self._serialize.url("interface_name", interface_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released + query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters) + response = self._client.send(request, header_parameters, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + header_dict = {} + + # @anusapan - deserialize as {object} from DigitalTwinInterfaces + if response.status_code == 200: + deserialized = self._deserialize('{object}', response) + header_dict = { + 'ETag': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + get_interface.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces/{interfaceName}'} + + def get_digital_twin_model( + self, model_id, expand=None, custom_headers=None, raw=False, **operation_config): + """Returns a DigitalTwin model definition for the given id. + If "expand" is present in the query parameters and id is for a device + capability model then it returns + the capability metamodel with expanded interface definitions. + + :param model_id: Model id Ex: + urn:contoso:TemperatureSensor:1 + :type model_id: str + :param expand: Indicates whether to expand the device capability + model's interface definitions inline or not. + This query parameter ONLY applies to Capability model. + :type expand: bool + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: class:`CloudError` + """ + # Construct URL + url = self.get_digital_twin_model.metadata['url'] + path_format_arguments = { + 'modelId': self._serialize.url("model_id", model_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + if expand is not None: + query_parameters['expand'] = self._serialize.query("expand", expand, 'bool') + # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released + query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters) + response = self._client.send(request, header_parameters, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'ETag': 'str', + 'x-ms-model-id': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + get_digital_twin_model.metadata = {'url': '/digitalTwins/models/{modelId}'} + + def invoke_interface_command( + self, digital_twin_id, interface_name, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config): + """Invoke a digital twin interface command. + + Invoke a digital twin interface command. + + :param digital_twin_id: + :type digital_twin_id: str + :param interface_name: + :type interface_name: str + :param command_name: + :type command_name: str + :param payload: + :type payload: object + :param connect_timeout_in_seconds: Connect timeout in seconds. + :type connect_timeout_in_seconds: int + :param response_timeout_in_seconds: Response timeout in seconds. + :type response_timeout_in_seconds: int + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: class:`CloudError` + """ + # Construct URL + url = self.invoke_interface_command.metadata['url'] + path_format_arguments = { + 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str'), + 'interfaceName': self._serialize.url("interface_name", interface_name, 'str'), + 'commandName': self._serialize.url("command_name", command_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released + query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') + if connect_timeout_in_seconds is not None: + query_parameters['connectTimeoutInSeconds'] = self._serialize.query("connect_timeout_in_seconds", connect_timeout_in_seconds, 'int') + if response_timeout_in_seconds is not None: + query_parameters['responseTimeoutInSeconds'] = self._serialize.query("response_timeout_in_seconds", response_timeout_in_seconds, 'int') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(payload, 'object') + + # Construct and send request + request = self._client.post(url, query_parameters) + response = self._client.send(request, header_parameters, body_content, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'x-ms-command-statuscode': 'int', + 'x-ms-request-id': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + invoke_interface_command.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces/{interfaceName}/commands/{commandName}'} diff --git a/azext_iot/service_sdk/models/__init__.py b/azext_iot/service_sdk/models/__init__.py index fbbb78115..aa0335a88 100644 --- a/azext_iot/service_sdk/models/__init__.py +++ b/azext_iot/service_sdk/models/__init__.py @@ -38,6 +38,16 @@ from .job_response import JobResponse from .module import Module from .cloud_to_device_method_result import CloudToDeviceMethodResult +from .desired import Desired +from .desired_state import DesiredState +from .digital_twin_interfaces import DigitalTwinInterfaces +from .digital_twin_interfaces_patch import DigitalTwinInterfacesPatch +from .digital_twin_interfaces_patch_interfaces_value import DigitalTwinInterfacesPatchInterfacesValue +from .digital_twin_interfaces_patch_interfaces_value_properties_value import DigitalTwinInterfacesPatchInterfacesValuePropertiesValue +from .digital_twin_interfaces_patch_interfaces_value_properties_value_desired import DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired +from .interface import Interface +from .property import Property +from .reported import Reported __all__ = [ 'ConfigurationMetrics', @@ -69,4 +79,14 @@ 'JobResponse', 'Module', 'CloudToDeviceMethodResult', + 'Desired', + 'DigitalTwinInterfaces', + 'DigitalTwinInterfacesPatch', + 'DigitalTwinInterfacesPatchInterfacesValue', + 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValue', + 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired', + 'DesiredState', + 'Interface', + 'Property', + 'Reported', ] diff --git a/azext_iot/service_sdk/models/desired.py b/azext_iot/service_sdk/models/desired.py new file mode 100644 index 000000000..8ac77f637 --- /dev/null +++ b/azext_iot/service_sdk/models/desired.py @@ -0,0 +1,29 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Desired(Model): + """Desired. + + :param value: The desired value of the interface property to set in a + digitalTwin. + :type value: object + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': 'object'}, + } + + def __init__(self, value=None): + super(Desired, self).__init__() + self.value = value diff --git a/azext_iot/service_sdk/models/desired_state.py b/azext_iot/service_sdk/models/desired_state.py new file mode 100644 index 000000000..5c29a0445 --- /dev/null +++ b/azext_iot/service_sdk/models/desired_state.py @@ -0,0 +1,40 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DesiredState(Model): + """DesiredState. + + :param code: Status code for the operation. + :type code: int + :param sub_code: Sub status code for the status. + :type sub_code: int + :param version: Version of the desired value received. + :type version: long + :param description: Description of the status. + :type description: str + """ + + _attribute_map = { + 'code': {'key': 'code', 'type': 'int'}, + 'sub_code': {'key': 'subCode', 'type': 'int'}, + 'version': {'key': 'version', 'type': 'long'}, + 'description': {'key': 'description', 'type': 'str'}, + } + + def __init__(self, code=None, sub_code=None, version=None, description=None): + super(DesiredState, self).__init__() + self.code = code + self.sub_code = sub_code + self.version = version + self.description = description diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces.py b/azext_iot/service_sdk/models/digital_twin_interfaces.py new file mode 100644 index 000000000..cac98248a --- /dev/null +++ b/azext_iot/service_sdk/models/digital_twin_interfaces.py @@ -0,0 +1,32 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinInterfaces(Model): + """DigitalTwinInterfaces. + + :param interfaces: Interface(s) data on the digital twin. + :type interfaces: dict[str, ~service.models.Interface] + :param version: Version of digital twin. + :type version: long + """ + + _attribute_map = { + 'interfaces': {'key': 'interfaces', 'type': '{Interface}'}, + 'version': {'key': 'version', 'type': 'long'}, + } + + def __init__(self, interfaces=None, version=None): + super(DigitalTwinInterfaces, self).__init__() + self.interfaces = interfaces + self.version = version diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces_patch.py b/azext_iot/service_sdk/models/digital_twin_interfaces_patch.py new file mode 100644 index 000000000..512ff1729 --- /dev/null +++ b/azext_iot/service_sdk/models/digital_twin_interfaces_patch.py @@ -0,0 +1,28 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinInterfacesPatch(Model): + """DigitalTwinInterfacesPatch. + + :param interfaces: Interface(s) data to patch in the digital twin. + :type interfaces: dict[str, ~service.models.DigitalTwinInterfacesPatchInterfacesValue] + """ + + _attribute_map = { + 'interfaces': {'key': 'interfaces', 'type': '{DigitalTwinInterfacesPatchInterfacesValue}'}, + } + + def __init__(self, interfaces=None): + super(DigitalTwinInterfacesPatch, self).__init__() + self.interfaces = interfaces diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value.py b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value.py new file mode 100644 index 000000000..1ce39941b --- /dev/null +++ b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value.py @@ -0,0 +1,28 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinInterfacesPatchInterfacesValue(Model): + """DigitalTwinInterfacesPatchInterfacesValue. + + :param properties: List of properties to update in an interface. + :type properties: dict[str, ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValue] + """ + + _attribute_map = { + 'properties': {'key': 'properties', 'type': '{DigitalTwinInterfacesPatchInterfacesValuePropertiesValue}'}, + } + + def __init__(self, properties=None): + super(DigitalTwinInterfacesPatchInterfacesValue, self).__init__() + self.properties = properties diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py new file mode 100644 index 000000000..c60a512bd --- /dev/null +++ b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py @@ -0,0 +1,28 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinInterfacesPatchInterfacesValuePropertiesValue(Model): + """DigitalTwinInterfacesPatchInterfacesValuePropertiesValue. + + :param desired: + :type desired: ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired + """ + + _attribute_map = { + 'desired': {'key': 'desired', 'type': 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired'}, + } + + def __init__(self, desired=None): + super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValue, self).__init__() + self.desired = desired diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py new file mode 100644 index 000000000..1e90b4098 --- /dev/null +++ b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py @@ -0,0 +1,29 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired(Model): + """DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired. + + :param value: The desired value of the interface property to set in a + digitalTwin. + :type value: object + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': 'object'}, + } + + def __init__(self, value=None): + super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired, self).__init__() + self.value = value diff --git a/azext_iot/service_sdk/models/interface.py b/azext_iot/service_sdk/models/interface.py new file mode 100644 index 000000000..b81ccd6cc --- /dev/null +++ b/azext_iot/service_sdk/models/interface.py @@ -0,0 +1,32 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Interface(Model): + """Interface. + + :param name: Full name of digital twin interface. + :type name: str + :param properties: List of all properties in an interface. + :type properties: dict[str, ~service.models.Property] + """ + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'properties': {'key': 'properties', 'type': '{Property}'}, + } + + def __init__(self, name=None, properties=None): + super(Interface, self).__init__() + self.name = name + self.properties = properties diff --git a/azext_iot/service_sdk/models/property.py b/azext_iot/service_sdk/models/property.py new file mode 100644 index 000000000..c950c11db --- /dev/null +++ b/azext_iot/service_sdk/models/property.py @@ -0,0 +1,32 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Property(Model): + """Property. + + :param reported: + :type reported: ~service.models.Reported + :param desired: + :type desired: ~service.models.Desired + """ + + _attribute_map = { + 'reported': {'key': 'reported', 'type': 'Reported'}, + 'desired': {'key': 'desired', 'type': 'Desired'}, + } + + def __init__(self, reported=None, desired=None): + super(Property, self).__init__() + self.reported = reported + self.desired = desired \ No newline at end of file diff --git a/azext_iot/service_sdk/models/reported.py b/azext_iot/service_sdk/models/reported.py new file mode 100644 index 000000000..1dc58b366 --- /dev/null +++ b/azext_iot/service_sdk/models/reported.py @@ -0,0 +1,32 @@ +# 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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Reported(Model): + """Reported. + + :param value: The current interface property value in a digitalTwin. + :type value: object + :param desired_state: + :type desired_state: ~service.models.DesiredState + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': 'object'}, + 'desired_state': {'key': 'desiredState', 'type': 'DesiredState'}, + } + + def __init__(self, value=None, desired_state=None): + super(Reported, self).__init__() + self.value = value + self.desired_state = desired_state diff --git a/azext_iot/service_sdk/version.py b/azext_iot/service_sdk/version.py index 9a6687264..729e580c0 100644 --- a/azext_iot/service_sdk/version.py +++ b/azext_iot/service_sdk/version.py @@ -9,5 +9,4 @@ # regenerated. # -------------------------------------------------------------------------- -VERSION = "2018-08-30-preview" - +VERSION = "2019-07-01-preview" diff --git a/azext_iot/tests/test_device_digitaltwin_interfaces.json b/azext_iot/tests/test_device_digitaltwin_interfaces.json new file mode 100644 index 000000000..22cf08ae5 --- /dev/null +++ b/azext_iot/tests/test_device_digitaltwin_interfaces.json @@ -0,0 +1,58 @@ +{ + "interfaces": { + "environmentalSensor": { + "name": "environmentalSensor", + "properties": { + "brightness": { + "desired": { + "value": 123 + }, + "reported": { + "desiredState": { + "code": 200, + "description": "Brightness updated", + "version": 4 + }, + "value": 123 + } + }, + "name": { + "desired": { + "value": "test" + }, + "reported": { + "desiredState": { + "code": 200, + "description": "Property Updated Successfully", + "version": 4 + }, + "value": "test" + } + }, + "state": { + "reported": { + "value": true + } + } + } + }, + "urn_azureiot_ModelDiscovery_DigitalTwin": { + "name": "urn_azureiot_ModelDiscovery_DigitalTwin", + "properties": { + "modelInformation": { + "reported": { + "value": { + "interfaces": { + "environmentalSensor": "urn:contoso:com:EnvironmentalSensor:1", + "urn_azureiot_ModelDiscovery_DigitalTwin": "urn:azureiot:ModelDiscovery:DigitalTwin:1", + "urn_azureiot_ModelDiscovery_ModelInformation": "urn:azureiot:ModelDiscovery:ModelInformation:1" + }, + "modelId": "urn:azureiot:testdevicecapabilitymodel:1" + } + } + } + } + } + }, + "version": 1 + } \ No newline at end of file diff --git a/azext_iot/tests/test_device_digitaltwin_invoke_command.json b/azext_iot/tests/test_device_digitaltwin_invoke_command.json new file mode 100644 index 000000000..fe34ec6b9 --- /dev/null +++ b/azext_iot/tests/test_device_digitaltwin_invoke_command.json @@ -0,0 +1,5 @@ +{ + "blinkRequest": { + "interval": 1 + } +} \ No newline at end of file diff --git a/azext_iot/tests/test_device_digitaltwin_property_update.json b/azext_iot/tests/test_device_digitaltwin_property_update.json new file mode 100644 index 000000000..09bcbd6b4 --- /dev/null +++ b/azext_iot/tests/test_device_digitaltwin_property_update.json @@ -0,0 +1,11 @@ +{ + "environmentalSensor": { + "properties": { + "name": { + "desired": { + "value": "test-update" + } + } + } + } +} \ No newline at end of file diff --git a/azext_iot/tests/test_iot_digitaltwin_unit.py b/azext_iot/tests/test_iot_digitaltwin_unit.py new file mode 100644 index 000000000..a662bed64 --- /dev/null +++ b/azext_iot/tests/test_iot_digitaltwin_unit.py @@ -0,0 +1,417 @@ +# 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. +# -------------------------------------------------------------------------------------------- +# pylint: disable=W0613,W0621 + +import pytest +import random +import json +import os +import sys + +from uuid import uuid4 +from azext_iot.operations import digitaltwin as subject +from azext_iot.operations.digitaltwin import INTERFACE_KEY_NAME +from azext_iot.common.utility import url_encode_str, validate_min_python_version +from knack.util import CLIError +from azure.cli.core.util import read_file_content + +# Path hack. +sys.path.insert(0, os.path.abspath('.')) + +_device_digitaltwin_invoke_command_payload = 'test_device_digitaltwin_invoke_command.json' +_device_digitaltwin_payload_file = 'test_device_digitaltwin_interfaces.json' +_device_digitaltwin_property_update_payload_file = 'test_device_digitaltwin_property_update.json' +_pnp_show_interface_file = 'test_pnp_interface_show.json' +_pnp_list_interface_file = 'test_pnp_interface_list.json' +path_iot_hub_monitor_events = 'azext_iot.operations.digitaltwin._iot_hub_monitor_events' +device_id = 'mydevice' +hub_entity = 'myhub.azure-devices.net' + +# Patch Paths # +path_mqtt_client = 'azext_iot.operations._mqtt.mqtt.Client' +path_service_client = 'msrest.service_client.ServiceClient.send' +path_ghcs = 'azext_iot.operations.hub.get_iot_hub_connection_string' +path_iot_hub_service_factory = 'azext_iot.common._azure.iot_hub_service_factory' +path_sas = 'azext_iot._factory.SasTokenAuthentication' + +mock_target = {} +mock_target['entity'] = hub_entity +mock_target['primarykey'] = 'rJx/6rJ6rmG4ak890+eW5MYGH+A0uzRvjGNjg3Ve8sfo=' +mock_target['secondarykey'] = 'aCd/6rJ6rmG4ak890+eW5MYGH+A0uzRvjGNjg3Ve8sfo=' +mock_target['policy'] = 'iothubowner' +mock_target['subscription'] = "5952cff8-bcd1-4235-9554-af2c0348bf23" +mock_target['location'] = "westus2" +mock_target['sku_tier'] = "Standard" +generic_cs_template = 'HostName={};SharedAccessKeyName={};SharedAccessKey={}' + + +def generate_cs(hub=hub_entity, policy=mock_target['policy'], key=mock_target['primarykey']): + return generic_cs_template.format(hub, policy, key) + + +mock_target['cs'] = generate_cs() + + +def change_dir(): + from inspect import getsourcefile + os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) + + +def generate_device_interfaces_payload(): + change_dir() + return json.loads(read_file_content(_device_digitaltwin_payload_file)) + + +def generate_pnp_interface_show_payload(): + change_dir() + return json.loads(read_file_content(_pnp_show_interface_file)) + + +def generate_pnp_interface_list_payload(): + change_dir() + return json.loads(read_file_content(_pnp_list_interface_file)) + + +def generate_device_digitaltwin_property_update_payload(content_from_file=False): + change_dir() + if content_from_file: + return (None, _device_digitaltwin_property_update_payload_file) + + return (str(read_file_content(_device_digitaltwin_property_update_payload_file)), + _device_digitaltwin_property_update_payload_file) + + +def generate_device_digitaltwin_invoke_command_payload(content_from_file=False): + change_dir() + if content_from_file: + return (None, _device_digitaltwin_invoke_command_payload) + + return (str(read_file_content(_device_digitaltwin_invoke_command_payload)), + _device_digitaltwin_invoke_command_payload) + + +@pytest.fixture() +def fixture_cmd(mocker): + # Placeholder for later use + mocker.patch(path_iot_hub_service_factory) + cmd = mocker.MagicMock(name='cli cmd context') + return cmd + + +@pytest.fixture() +def fixture_ghcs(mocker): + ghcs = mocker.patch(path_ghcs) + ghcs.return_value = mock_target + return ghcs + + +@pytest.fixture(params=[400, 401, 500]) +def serviceclient_generic_error(mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {'error': 'something failed'}) + return service_client + + +@pytest.fixture() +def fixture_monitor_events(mocker): + mocker.patch(path_iot_hub_monitor_events) + + +def build_mock_response(mocker, status_code=200, payload=None, headers=None, raw=False): + response = mocker.MagicMock(name='response') + response.status_code = status_code + del response.context + del response._attribute_map + + if raw: + response.text = payload + else: + response.text.return_value = json.dumps(payload) + + if headers: + response.headers = headers + return response + + +class TestDTInterfaceList(object): + + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, request): + service_client = mocker.patch(path_service_client) + output = generate_device_interfaces_payload() + service_client.return_value = build_mock_response(mocker, request.param, output) + return service_client + + def test_iot_digitaltwin_interface_list(self, fixture_cmd, serviceclient): + result = subject.iot_digitaltwin_interface_list(fixture_cmd, device_id=device_id, + login=mock_target['cs']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + + assert method == 'GET' + assert '{}/digitalTwins/{}/interfaces/{}?'.format(mock_target['entity'], device_id, + INTERFACE_KEY_NAME) in url + assert json.dumps(result) + assert len(result['interfaces']) == 3 + + @pytest.mark.parametrize("exp", [(CLIError)]) + def test_iot_digitaltwin_interface_list_error(self, fixture_cmd, serviceclient_generic_error, exp): + with pytest.raises(exp): + subject.iot_digitaltwin_interface_list(fixture_cmd, device_id=device_id, + login=mock_target['cs']) + + +class TestDTCommandList(object): + + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + output = generate_device_interfaces_payload() + payload_list = generate_pnp_interface_list_payload() + payload_show = generate_pnp_interface_show_payload() + test_side_effect = [ + build_mock_response(mocker, request.param, payload=output), + build_mock_response(mocker, request.param, payload=payload_list), + build_mock_response(mocker, request.param, payload=payload_show) + ] + service_client.side_effect = test_side_effect + return service_client + + def test_iot_digitaltwin_command_list(self, fixture_cmd, serviceclient): + result = subject.iot_digitaltwin_command_list(fixture_cmd, device_id=device_id, source_model='public', + interface='environmentalSensor', login=mock_target['cs']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + + assert method == 'GET' + assert '/models/' in url + assert json.dumps(result) + assert len(result['interfaces']) == 1 + assert result['interfaces'][0]['name'] == 'environmentalSensor' + assert len(result['interfaces'][0]['commands']) == 3 + + @pytest.mark.parametrize("exp", [(CLIError)]) + def test_iot_digitaltwin_command_list_error(self, fixture_cmd, serviceclient_generic_error, exp): + with pytest.raises(exp): + subject.iot_digitaltwin_command_list(fixture_cmd, device_id=device_id, source_model='public', + login=mock_target['cs']) + + @pytest.mark.parametrize("interface, exp", [('inter1', CLIError)]) + def test_iot_digitaltwin_command_list_args_error(self, fixture_cmd, serviceclient, interface, exp): + with pytest.raises(exp): + subject.iot_digitaltwin_command_list(fixture_cmd, device_id=device_id, interface=interface, + login=mock_target['cs'], source_model='public') + + +class TestDTPropertiesList(object): + + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + output = generate_device_interfaces_payload() + payload_list = generate_pnp_interface_list_payload() + payload_show = generate_pnp_interface_show_payload() + test_side_effect = [ + build_mock_response(mocker, request.param, payload=output), + build_mock_response(mocker, request.param, payload=payload_list), + build_mock_response(mocker, request.param, payload=payload_show) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("target_interface", [('environmentalSensor')]) + def test_iot_digitaltwin_properties_list(self, fixture_cmd, serviceclient, target_interface): + result = subject.iot_digitaltwin_properties_list(fixture_cmd, device_id=device_id, source_model='public', + interface=target_interface, login=mock_target['cs']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + + assert method == 'GET' + assert '/models/' in url + assert json.dumps(result) + if target_interface: + assert len(result['interfaces']) == 1 + assert result['interfaces'][0]['name'] == 'environmentalSensor' + assert len(result['interfaces'][0]['properties']) == 3 + + @pytest.mark.parametrize("exp", [(CLIError)]) + def test_iot_digitaltwin_properties_list_error(self, fixture_cmd, serviceclient_generic_error, exp): + with pytest.raises(exp): + subject.iot_digitaltwin_properties_list(fixture_cmd, device_id=device_id, source_model='public', + login=mock_target['cs']) + + @pytest.mark.parametrize("interface, exp", [('inter1', CLIError)]) + def test_iot_digitaltwin_properties_list_args_error(self, fixture_cmd, serviceclient, interface, exp): + with pytest.raises(exp): + subject.iot_digitaltwin_properties_list(fixture_cmd, device_id=device_id, interface=interface, + login=mock_target['cs'], source_model='public') + + +class TestDTPropertyUpdate(object): + + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.mark.parametrize("payload_scenario", [ + (generate_device_digitaltwin_property_update_payload()), + (generate_device_digitaltwin_property_update_payload(content_from_file=True))]) + def test_iot_digitaltwin_property_update(self, fixture_cmd, serviceclient, payload_scenario): + + payload = None + + # If file path provided + if not payload_scenario[0]: + payload = payload_scenario[1] + else: + payload = str(read_file_content(_device_digitaltwin_property_update_payload_file)) + + subject.iot_digitaltwin_property_update(fixture_cmd, device_id=device_id, + interface_payload=payload, login=mock_target['cs']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + + assert method == 'PATCH' + assert '{}/digitalTwins/{}/interfaces?'.format(mock_target['entity'], device_id) in url + + @pytest.mark.parametrize("payload_scenario", [(generate_device_digitaltwin_property_update_payload())]) + def test_iot_digitaltwin_property_update_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario): + with pytest.raises(CLIError): + subject.iot_digitaltwin_property_update(fixture_cmd, device_id=device_id, + interface_payload=payload_scenario[0], + login=mock_target['cs']) + + +class TestDTInvokeCommand(object): + + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + output = generate_device_interfaces_payload() + payload_list = generate_pnp_interface_list_payload() + payload_show = generate_pnp_interface_show_payload() + test_side_effect = [ + build_mock_response(mocker, request.param, payload=output), + build_mock_response(mocker, request.param, payload=payload_list), + build_mock_response(mocker, request.param, payload=payload_show), + build_mock_response(mocker, request.param, {}) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("command_payload", [ + (generate_device_digitaltwin_invoke_command_payload()), + (generate_device_digitaltwin_invoke_command_payload(content_from_file=True))]) + def test_iot_digitaltwin_invoke_command(self, fixture_cmd, serviceclient, command_payload): + + payload = None + interface = 'environmentalSensor' + command = 'blink' + + # If file path provided + if not command_payload[0]: + payload = command_payload[1] + else: + payload = str(read_file_content(_device_digitaltwin_invoke_command_payload)) + + subject.iot_digitaltwin_invoke_command(fixture_cmd, device_id=device_id, + interface=interface, + command_name=command, + command_payload=payload, + login=mock_target['cs']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + + assert method == 'POST' + assert '{}/digitalTwins/{}/interfaces/{}/commands/{}?'.format(mock_target['entity'], device_id, + interface, command) in url + + @pytest.fixture(params=[(200, 400), (200, 401), (200, 500)]) + def serviceclient_error(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + output = generate_device_interfaces_payload() + test_side_effect = [ + build_mock_response(mocker, request.param[0], payload=output), + build_mock_response(mocker, request.param[1], {}) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("command_payload, interface, command", [ + (generate_device_digitaltwin_invoke_command_payload(), 'environmentalSensor', 'blink'), + (generate_device_digitaltwin_invoke_command_payload(content_from_file=True), 'environmentalSensor', 'blink'), + (generate_device_digitaltwin_invoke_command_payload(), 'test', 'blink'), + (generate_device_digitaltwin_invoke_command_payload(), 'environmentalSensor', 'test')]) + def test_iot_digitaltwin_property_update_error(self, fixture_cmd, serviceclient_error, + command_payload, interface, command): + payload = None + if not command_payload[0]: + payload = command_payload[1] + else: + payload = str(read_file_content(_device_digitaltwin_invoke_command_payload)) + with pytest.raises(CLIError): + subject.iot_digitaltwin_invoke_command(fixture_cmd, device_id=device_id, + interface=interface, + command_name=command, + command_payload=payload, + login=mock_target['cs']) + + +@pytest.mark.skipif(not validate_min_python_version(3, 5, exit_on_fail=False), reason="minimum python version not satisfied") +class TestDTMonitorEvents(object): + + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, fixture_ghcs, fixture_monitor_events, request): + service_client = mocker.patch(path_service_client) + output = generate_device_interfaces_payload() + payload_list = generate_pnp_interface_list_payload() + payload_show = generate_pnp_interface_show_payload() + test_side_effect = [ + build_mock_response(mocker, request.param, payload=output), + build_mock_response(mocker, request.param, payload=payload_list), + build_mock_response(mocker, request.param, payload=payload_show) + ] + service_client.side_effect = test_side_effect + return service_client + + def test_iot_digitaltwin_monitor_events(self, fixture_cmd, serviceclient): + subject.iot_digitaltwin_monitor_events(fixture_cmd, device_id=device_id, + interface='environmentalSensor', source_model='public', + yes=True, properties='all', login=mock_target['cs']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + + assert method == 'GET' + assert '/models/' in url + + @pytest.fixture(params=[200]) + def serviceclienterror(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + output = generate_device_interfaces_payload() + payload_list = generate_pnp_interface_list_payload() + payload_show = generate_pnp_interface_show_payload() + test_side_effect = [ + build_mock_response(mocker, request.param, payload=output), + build_mock_response(mocker, request.param, payload=payload_list), + build_mock_response(mocker, request.param, payload=payload_show) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("interface, timeout", [('inter1', 0), ('environmentalSensor', -1)]) + def test_iot_digitaltwin_monitor_events_error(self, fixture_cmd, serviceclienterror, interface, timeout): + with pytest.raises(CLIError): + subject.iot_digitaltwin_monitor_events(fixture_cmd, device_id=device_id, source_model='public', + interface=interface, timeout=timeout, + yes=True, properties='all', login=mock_target['cs']) diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 608ff5b90..f88e07d0a 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -1336,8 +1336,8 @@ def test_hub_monitor_events(self): LIVE_HUB, LIVE_RG, LIVE_CONSUMER_GROUPS[0], enqueued_time), ['{\\r\\n\\"payload_data1\\"\\"payload_value1\\"\\r\\n}']) for cg in LIVE_CONSUMER_GROUPS: - self.cmd('az iot hub consumer-group delete --hub-name {} --resource-group {} --name {}'.format(LIVE_HUB, LIVE_RG, cg), - expect_failure=False) + self.cmd('az iot hub consumer-group delete --hub-name {} --resource-group {} --name {}' + .format(LIVE_HUB, LIVE_RG, cg), expect_failure=False) @pytest.mark.skipif(not validate_min_python_version(3, 4, exit_on_fail=False), reason="minimum python version not satisfied") def test_hub_monitor_feedback(self): diff --git a/azext_iot/tests/test_iot_pnp_int.py b/azext_iot/tests/test_iot_pnp_int.py new file mode 100644 index 000000000..6ab827e2c --- /dev/null +++ b/azext_iot/tests/test_iot_pnp_int.py @@ -0,0 +1,174 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Unpublished works. +# -------------------------------------------------------------------------------------------- +import pytest +import random +import json +import os +import sys + +from os.path import exists +from uuid import uuid4 +from azure.cli.testsdk import LiveScenarioTest +from azure.cli.core.util import read_file_content + + +# Add test tools to path +sys.path.append(os.path.abspath(os.path.join('.', 'iotext_test_tools'))) + +# Set these to the proper PnP Endpoint, PnP Cstring and PnP Repository for Live Integration Tests. +_endpoint = os.environ.get('azext_pnp_endpoint') +_repo_id = os.environ.get('azext_pnp_repository') +_repo_cs = os.environ.get('azext_pnp_cs') + +_interface_payload = 'test_pnp_create_payload_interface.json' +_capability_model_payload = 'test_pnp_create_payload_model.json' + +if not all([_endpoint, _repo_id, _repo_cs]): + raise ValueError('Set azext_pnp_endpoint, azext_pnp_repository and azext_pnp_cs to run PnP model integration tests.') + + +def change_dir(): + from inspect import getsourcefile + os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) + + +class TestPnPModel(LiveScenarioTest): + + rand_val = random.randint(1, 10001) + + def __init__(self, _): + from iotext_test_tools import DummyCliOutputProducer + super(TestPnPModel, self).__init__(_) + self.cli_ctx = DummyCliOutputProducer() + self.kwargs.update({ + 'endpoint': _endpoint, + 'repo': _repo_id, + 'repo_cs': _repo_cs, + 'interface': 'test_interface_definition.json', + 'interface-updated': 'test_interface_updated_definition.json', + 'model': 'test_model_definition.json', + 'model-updated': 'test_model_updated_definition.json' + }) + + def setUp(self): + change_dir() + if(self._testMethodName == 'test_interface_life_cycle'): + interface = str(read_file_content(_interface_payload)) + _interface_id = '{}{}'.format(json.loads(interface)['@id'], TestPnPModel.rand_val) + self.kwargs.update({'interface_id': _interface_id}) + interface_newContent = interface.replace(json.loads(interface)['@id'], self.kwargs['interface_id']) + interface_newContent = interface_newContent.replace('\n', '') + + fo = open(self.kwargs['interface'], "w+", encoding='utf-8') + fo.write(interface_newContent) + fo.close() + + if(self._testMethodName == 'test_model_life_cycle'): + model = str(read_file_content(_capability_model_payload)) + _model_id = '{}{}'.format(json.loads(model)['@id'], TestPnPModel.rand_val) + self.kwargs.update({'model_id': _model_id}) + model_newContent = model.replace(json.loads(model)['@id'], self.kwargs['model_id']) + model_newContent = model_newContent.replace('\n', '') + + fo = open(self.kwargs['model'], "w+", encoding='utf-8') + fo.write(model_newContent) + fo.close() + + def tearDown(self): + change_dir() + if exists(self.kwargs['interface-updated']): + os.remove(self.kwargs['interface-updated']) + if exists(self.kwargs['model-updated']): + os.remove(self.kwargs['model-updated']) + if exists(self.kwargs['interface']): + os.remove(self.kwargs['interface']) + if exists(self.kwargs['model']): + os.remove(self.kwargs['model']) + + def test_interface_life_cycle(self): + + # Error: missing repo-id or login + self.cmd('iot pnp interface create -e {endpoint} --def {interface}', expect_failure=True) + + # Error: Invalid Interface definition file + self.cmd('iot pnp interface create -e {endpoint} -r {repo} --def interface', expect_failure=True) + + # Error: wrong path of Interface definition + self.cmd('iot pnp interface create -e {endpoint} -r {repo} --def interface.json', expect_failure=True) + + # Success: Create new Interface + self.cmd('iot pnp interface create -e {endpoint} -r {repo} --def {interface}', checks=self.is_empty()) + + # Checking the Interface list + self.cmd('iot pnp interface list -e {endpoint} -r {repo}', + checks=[self.greater_than('length([*])', 0)]) + + # Get Interface + interface = self.cmd('iot pnp interface show -e {endpoint} -r {repo} -i {interface_id}').get_output_in_json() + assert json.dumps(interface) + assert interface['@id'] == self.kwargs['interface_id'] + assert interface['displayName'] == 'MXChip1' + assert len(interface['contents']) > 0 + + # Success: Update Interface + interface = str(read_file_content(self.kwargs['interface'])) + display_name = json.loads(interface)['displayName'] + interface_newContent = interface.replace(display_name, '{}-Updated'.format(display_name)) + interface_newContent = interface_newContent.replace('\n', '') + fo = open(self.kwargs['interface-updated'], "w+", encoding='utf-8') + fo.write(interface_newContent) + fo.close() + self.cmd('iot pnp interface update -e {endpoint} -r {repo} --def {interface-updated}', checks=self.is_empty()) + + # Todo: Publish Interface + self.cmd('iot pnp interface publish -e {endpoint} -r {repo} -i {interface_id}', checks=self.is_empty()) + + # Success: Delete Interface + self.cmd('iot pnp interface delete -e {endpoint} -r {repo} -i {interface_id}', checks=self.is_empty()) + + def test_model_life_cycle(self): + + # Checking the Capability-Model list + self.cmd('iot pnp capability-model list -e {endpoint} -r {repo}', + checks=[self.check('length([*])', 0)]) + + # Error: missing repo-id or login + self.cmd('iot pnp capability-model create -e {endpoint} --def {model}', expect_failure=True) + + # Error: Invalid Capability-Model definition file + self.cmd('iot pnp capability-model create -e {endpoint} -r {repo} --def model', expect_failure=True) + + # Error: wrong path of Capability-Model definition + self.cmd('iot pnp capability-model create -e {endpoint} -r {repo} --def model.json', expect_failure=True) + + # Success: Create new Capability-Model + self.cmd('iot pnp capability-model create -e {endpoint} -r {repo} --def {model}', checks=self.is_empty()) + + # Checking the Capability-Model list + self.cmd('iot pnp capability-model list -e {endpoint} -r {repo}', + checks=[self.check('length([*])', 1)]) + + # Get Capability-Model + model = self.cmd('iot pnp capability-model show -e {endpoint} -r {repo} -m {model_id}').get_output_in_json() + assert json.dumps(model) + assert model['@id'] == self.kwargs['model_id'] + assert len(model['implements']) > 0 + + # Success: Update Capability-Model + model = str(read_file_content(self.kwargs['model'])) + display_name = json.loads(model)['displayName'] + model_newContent = model.replace(display_name, '{}-Updated'.format(display_name)) + model_newContent = model_newContent.replace('\n', '') + fo = open(self.kwargs['model-updated'], "w+", encoding='utf-8') + fo.write(model_newContent) + fo.close() + self.cmd('iot pnp capability-model update -e {endpoint} -r {repo} --def {model-updated}', checks=self.is_empty()) + + # Todo: Publish Capability-Model + self.cmd('iot pnp capability-model publish -e {endpoint} -r {repo} -m {model_id}', checks=self.is_empty()) + + # Success: Delete Capability-Model + self.cmd('iot pnp capability-model delete -e {endpoint} -r {repo} -m {model_id}', checks=self.is_empty()) diff --git a/azext_iot/tests/test_iot_pnp_unit.py b/azext_iot/tests/test_iot_pnp_unit.py new file mode 100644 index 000000000..5f6fd08da --- /dev/null +++ b/azext_iot/tests/test_iot_pnp_unit.py @@ -0,0 +1,791 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Unpublished works. +# -------------------------------------------------------------------------------------------- +# pylint: disable=W0613,W0621 +# flake8: noqa + + +import pytest +import random +import json +import os +import sys + +from uuid import uuid4 +from azext_iot.operations import pnp as subject +from azext_iot.common.utility import url_encode_str +from knack.util import CLIError +from azure.cli.core.util import read_file_content + + +# Path hack. +sys.path.insert(0, os.path.abspath('.')) + +from test_iot_ext_unit import (fixture_cmd, + fixture_sas, + path_service_client, + serviceclient_generic_error, + build_mock_response) + +_repo_endpoint = 'https://{}.{}'.format(str(uuid4()), 'com') +_repo_id = str(uuid4()).replace('-', '') +_repo_keyname = str(uuid4()).replace('-', '') +_repo_secret = 'lMT+wSy8TIzDASRMlhxwpYxG3mWba45YqCFUo6Qngju5uZS9V4tM2yh5pn3zdB0FC3yRx91UnSWjdr/jLutPbg==' +generic_cs_template = 'HostName={};RepositoryId={};SharedAccessKeyName={};SharedAccessKey={}' +path_ghcs = 'azext_iot.operations.pnp.get_iot_pnp_connection_string' +_pnp_create_interface_payload_file = 'test_pnp_create_payload_interface.json' +_pnp_create_model_payload_file = 'test_pnp_create_payload_model.json' +_pnp_show_interface_file = 'test_pnp_interface_show.json' +_pnp_generic_interface_id = 'urn:example:interfaces:MXChip:1' +_pnp_generic_model_id = 'urn:example:capabilityModels:Mxchip:1' + +@pytest.fixture() +def fixture_ghcs(mocker): + ghcs = mocker.patch(path_ghcs) + ghcs.return_value = mock_target + return ghcs + +def generate_cs(endpoint=_repo_endpoint, repository=_repo_id, policy=_repo_keyname, key=_repo_secret): + return generic_cs_template.format(endpoint, repository, policy, key) + +def change_dir(): + from inspect import getsourcefile + os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) + +mock_target = {} +mock_target['cs'] = generate_cs() +mock_target['policy'] = _repo_keyname +mock_target['primarykey'] = _repo_secret +mock_target['repository_id'] = _repo_id +mock_target['entity'] = _repo_endpoint +mock_target['entity'] = mock_target['entity'].replace('https://', '') +mock_target['entity'] = mock_target['entity'].replace('http://', '') + +def generate_pnp_interface_create_payload(content_from_file=False): + change_dir() + if content_from_file: + return (None, _pnp_create_interface_payload_file) + + return (str(read_file_content(_pnp_create_interface_payload_file)), _pnp_create_interface_payload_file) + + +class TestModelRepoInterfaceCreate(object): + @pytest.fixture(params=[201, 204, 412]) + def serviceclient(self, mocker, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.mark.parametrize("payload_scenario", [(generate_pnp_interface_create_payload()), + (generate_pnp_interface_create_payload(content_from_file=True))]) + def test_interface_create(self, fixture_cmd, serviceclient, payload_scenario): + payload = None + + # If file path provided + if not payload_scenario[0]: + payload = payload_scenario[1] + else: + payload = str(read_file_content(_pnp_create_interface_payload_file)) + + subject.iot_pnp_interface_create(fixture_cmd, interface_definition=payload, + login=mock_target['cs']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + data = args[0][0].data + headers = args[0][0].headers + + assert method == 'PUT' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert json.dumps(data) + assert headers.get('Authorization') + + @pytest.mark.parametrize("payload_scenario", [(generate_pnp_interface_create_payload())]) + def test_interface_create_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario): + with pytest.raises(CLIError): + subject.iot_pnp_interface_create(fixture_cmd, + login=mock_target['cs'], + interface_definition=payload_scenario[0]) + + @pytest.mark.parametrize("payload_scenario, exp", [ + (generate_pnp_interface_create_payload(), CLIError), + ]) + def test_interface_create_invalid_args(self, serviceclient, payload_scenario, exp): + with pytest.raises(exp): + subject.iot_pnp_interface_create(fixture_cmd, + interface_definition=payload_scenario) + + def test_interface_create_invalid_payload(self, serviceclient): + + payload = str(read_file_content(_pnp_create_interface_payload_file)) + payload = json.loads(payload) + del payload['@id'] + payload = json.dumps(payload) + with pytest.raises(CLIError): + subject.iot_pnp_interface_create(fixture_cmd, login=mock_target['cs'], + interface_definition=payload) + + +class TestModelRepoInterfaceUpdate(object): + + @pytest.fixture(params=[(200, 201), (200, 204), (200, 412)]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + payload = { + "continuationToken": "null", + "results": [ + { + "comment": "", + "createdOn": "2019-07-09T07:46:06.044161+00:00", + "description": "", + "displayName": "MXChip 1", + "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"", + "modelName": "example:interfaces:MXChip", + "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", + "publisherName": "microsoft.com", + "type": "Interface", + "updatedOn": "2019-07-09T21:06:00.072063+00:00", + "urnId": "urn:example:interfaces:MXChip:1", + "version": 1 + } + ] + } + test_side_effect = [ + build_mock_response(mocker, request.param[0], payload=payload), + build_mock_response(mocker, request.param[1], {}) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("payload_scenario", + [generate_pnp_interface_create_payload(), + generate_pnp_interface_create_payload(content_from_file=True)]) + def test_interface_update(self, fixture_cmd, serviceclient, payload_scenario): + payload = None + + # If file path provided + if not payload_scenario[0]: + payload = payload_scenario[1] + else: + payload = str(read_file_content(_pnp_create_interface_payload_file)) + + subject.iot_pnp_interface_update(fixture_cmd, interface_definition=payload, + repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + data = args[0][0].data + headers = args[0][0].headers + + assert method == 'PUT' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert json.dumps(data) + assert headers.get('Authorization') + + @pytest.mark.parametrize("payload_scenario", + [(generate_pnp_interface_create_payload()), + (generate_pnp_interface_create_payload(content_from_file=True))]) + def test_model_update_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario): + if not payload_scenario[0]: + payload = payload_scenario[1] + else: + payload = str(read_file_content(_pnp_create_interface_payload_file)) + with pytest.raises(CLIError): + subject.iot_pnp_interface_update(fixture_cmd, interface_definition=payload, + repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id']) + + @pytest.mark.parametrize("payload_scenario, exp", [ + (generate_pnp_interface_create_payload(), CLIError), + ]) + def test_interface_update_invalid_args(self, serviceclient, payload_scenario, exp): + with pytest.raises(exp): + subject.iot_pnp_interface_update(fixture_cmd, interface_definition=payload_scenario) + + def test_interface_update_invalid_payload(self, serviceclient): + + payload = str(read_file_content(_pnp_create_interface_payload_file)) + payload = json.loads(payload) + payload['@id'] = 'fake_invalid_id' + payload = json.dumps(payload) + with pytest.raises(CLIError): + subject.iot_pnp_interface_update(fixture_cmd, interface_definition=payload, + repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id']) + + +class TestModelRepoInterfacePublish(object): + + @pytest.fixture(params=[(200, 200, 201), (200, 200, 204), (200, 200, 412)]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + payload_list = { + "continuationToken": "null", + "results": [ + { + "comment": "", + "createdOn": "2019-07-09T07:46:06.044161+00:00", + "description": "", + "displayName": "MXChip 1", + "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"", + "modelName": "example:interfaces:MXChip", + "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", + "publisherName": "microsoft.com", + "type": "Interface", + "updatedOn": "2019-07-09T21:06:00.072063+00:00", + "urnId": "urn:example:interfaces:MXChip:1", + "version": 1 + } + ] + } + payload_show = { + "@id": "urn:example:interfaces:MXChip:1", + "@type": "Interface", + "displayName": "MXChip 1", + "contents": [ + { + "@type": "Property", + "displayName": "Die Number", + "name": "dieNumber", + "schema": "double" + } + ], + "@context": "http://azureiot.com/v1/contexts/Interface.json" + } + test_side_effect = [ + build_mock_response(mocker, request.param[0], payload=payload_list), + build_mock_response(mocker, request.param[1], payload=payload_show), + build_mock_response(mocker, request.param[2], {}) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)]) + def test_interface_publish(self, fixture_cmd, serviceclient, target_interface): + subject.iot_pnp_interface_publish(fixture_cmd, interface=target_interface, + login=mock_target['cs']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + data = args[0][0].data + headers = args[0][0].headers + + assert method == 'PUT' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url + assert json.loads(data)["@id"] == _pnp_generic_interface_id + assert headers.get('Authorization') + + @pytest.mark.parametrize("target_interface", [('acv.17')]) + def test_interface_publish_error(self, fixture_cmd, serviceclient_generic_error, target_interface): + with pytest.raises(CLIError): + subject.iot_pnp_interface_publish(fixture_cmd, interface=target_interface, + login=mock_target['cs']) + + +class TestModelRepoInterfaceShow(object): + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + payload = { + "@id": "urn:example:interfaces:MXChip:1", + "@type": "Interface", + "displayName": "MXChip 1", + "contents": [ + { + "@type": "Property", + "displayName": "Die Number", + "name": "dieNumber", + "schema": "double" + } + ], + "@context": "http://azureiot.com/v1/contexts/Interface.json" + } + service_client.return_value = build_mock_response(mocker, request.param, payload) + return service_client + + @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)]) + def test_interface_show(self, fixture_cmd, serviceclient, target_interface): + result = subject.iot_pnp_interface_show(fixture_cmd, + interface=target_interface, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + headers = args[0][0].headers + + assert method == 'GET' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert json.dumps(result) + assert headers.get('Authorization') + + @pytest.mark.parametrize("target_interface", [('int:123')]) + def test_interface_show_error(self, fixture_cmd, serviceclient_generic_error, target_interface): + with pytest.raises(CLIError): + result = subject.iot_pnp_interface_show(fixture_cmd, + interface=target_interface, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + + @pytest.fixture(params=[200]) + def serviceclientemptyresult(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)]) + def test_interface_show_error(self, fixture_cmd, serviceclientemptyresult, target_interface): + with pytest.raises(CLIError): + result = subject.iot_pnp_interface_show(fixture_cmd, + interface=target_interface, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + + +class TestModelRepoInterfaceList(object): + @pytest.fixture(params=[200]) + def service_client(self, mocker, fixture_ghcs, request): + serviceclient = mocker.patch(path_service_client) + payload = { + "continuationToken": "null", + "results": [ + { + "comment": "", + "createdOn": "2019-07-09T07:46:06.044161+00:00", + "description": "", + "displayName": "MXChip 1", + "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"", + "modelName": "example:interfaces:MXChip", + "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", + "publisherName": "microsoft.com", + "type": "Interface", + "updatedOn": "2019-07-09T21:06:00.072063+00:00", + "urnId": "urn:example:interfaces:MXChip:1", + "version": 1 + } + ] + } + serviceclient.return_value = build_mock_response(mocker, request.param, payload) + return serviceclient + + def test_interface_list(self, fixture_cmd, service_client): + result = subject.iot_pnp_interface_list(fixture_cmd, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + args = service_client.call_args + url = args[0][0].url + method = args[0][0].method + headers = args[0][0].headers + + assert method == 'POST' + assert '{}/models/search?'.format(_repo_endpoint) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert len(result) == 1 + assert headers.get('Authorization') + + def test_interface_list_error(self, fixture_cmd, serviceclient_generic_error): + with pytest.raises(CLIError): + subject.iot_pnp_interface_list(fixture_cmd, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + + +class TestModelRepoInterfaceDelete(object): + @pytest.fixture(params=[204]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)]) + def test_interface_delete(self, fixture_cmd, serviceclient, target_interface): + subject.iot_pnp_interface_delete(fixture_cmd, + interface=target_interface, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + headers = args[0][0].headers + + assert method == 'DELETE' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert headers.get('Authorization') + + @pytest.mark.parametrize("target_interface", [('acv.17')]) + def test_model_delete_error(self, fixture_cmd, serviceclient_generic_error, target_interface): + with pytest.raises(CLIError): + subject.iot_pnp_interface_delete(fixture_cmd, + interface=target_interface, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + + +def generate_pnp_model_create_payload(content_from_file=False): + change_dir() + if content_from_file: + return (None, _pnp_create_model_payload_file) + + return (str(read_file_content(_pnp_create_model_payload_file)), _pnp_create_model_payload_file) + + +class TestModelRepoModelCreate(object): + + @pytest.fixture(params=[201, 204, 412]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.mark.parametrize("payload_scenario", + [(generate_pnp_model_create_payload()), + (generate_pnp_model_create_payload(content_from_file=True))]) + def test_model_create(self, fixture_cmd, serviceclient, payload_scenario): + + payload = None + + # If file path provided + if not payload_scenario[0]: + payload = payload_scenario[1] + else: + payload = str(read_file_content(_pnp_create_model_payload_file)) + + subject.iot_pnp_model_create(fixture_cmd, model_definition=payload, + repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + data = args[0][0].data + headers = args[0][0].headers + + assert method == 'PUT' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert json.dumps(data) + assert headers.get('Authorization') + + @pytest.mark.parametrize("payload_scenario", + [(generate_pnp_model_create_payload()), + (generate_pnp_model_create_payload(content_from_file=True))]) + def test_model_create_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario): + if not payload_scenario[0]: + payload = payload_scenario[1] + else: + payload = str(read_file_content(_pnp_create_model_payload_file)) + with pytest.raises(CLIError): + subject.iot_pnp_model_create(fixture_cmd, model_definition=payload, + repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id']) + + @pytest.mark.parametrize("payload_scenario, exp", [ + (generate_pnp_model_create_payload(), CLIError), + ]) + def test_model_create_invalid_args(self, serviceclient, payload_scenario, exp): + with pytest.raises(exp): + subject.iot_pnp_model_create(fixture_cmd, model_definition=payload_scenario) + + def test_model_create_invalid_payload(self, serviceclient): + + payload = str(read_file_content(_pnp_create_model_payload_file)) + payload = json.loads(payload) + del payload['@id'] + payload = json.dumps(payload) + with pytest.raises(CLIError): + subject.iot_pnp_model_create(fixture_cmd, login=mock_target['cs'], + model_definition=payload) + + +class TestModelRepoModelUpdate(object): + + @pytest.fixture(params=[(200, 201), (200, 204), (200, 412)]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + payload = { + "continuationToken": "null", + "results": [ + { + "comment": "", + "createdOn": "2019-07-09T07:46:06.044161+00:00", + "description": "", + "displayName": "Mxchip 1", + "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"", + "modelName": "example:capabilityModels:Mxchip", + "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", + "publisherName": "microsoft.com", + "type": "CapabilityModel", + "updatedOn": "2019-07-09T21:06:00.072063+00:00", + "urnId": "urn:example:capabilityModels:Mxchip:1", + "version": 1 + } + ] + } + test_side_effect = [ + build_mock_response(mocker, request.param[0], payload=payload), + build_mock_response(mocker, request.param[1], {}) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("payload_scenario", + [generate_pnp_model_create_payload(), + generate_pnp_model_create_payload(content_from_file=True)]) + def test_model_update(self, fixture_cmd, serviceclient, payload_scenario): + + payload = None + + # If file path provided + if not payload_scenario[0]: + payload = payload_scenario[1] + else: + payload = str(read_file_content(_pnp_create_model_payload_file)) + + subject.iot_pnp_model_update(fixture_cmd, model_definition=payload, + repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + data = args[0][0].data + headers = args[0][0].headers + + assert method == 'PUT' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert json.dumps(data) + assert headers.get('Authorization') + + @pytest.mark.parametrize("payload_scenario", + [(generate_pnp_model_create_payload()), + (generate_pnp_model_create_payload(content_from_file=True))]) + def test_model_update_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario): + if not payload_scenario[0]: + payload = payload_scenario[1] + else: + payload = str(read_file_content(_pnp_create_model_payload_file)) + with pytest.raises(CLIError): + subject.iot_pnp_model_update(fixture_cmd, model_definition=payload, + repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id']) + + @pytest.mark.parametrize("payload_scenario, exp", [ + (generate_pnp_model_create_payload(), CLIError), + ]) + def test_model_update_invalid_args(self, serviceclient, payload_scenario, exp): + with pytest.raises(exp): + subject.iot_pnp_model_update(fixture_cmd, model_definition=payload_scenario) + + def test_model_update_invalid_payload(self, serviceclient): + + payload = str(read_file_content(_pnp_create_model_payload_file)) + payload = json.loads(payload) + payload['@id'] = 'fake_invalid_id' + payload = json.dumps(payload) + with pytest.raises(CLIError): + subject.iot_pnp_model_update(fixture_cmd, model_definition=payload, + repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id']) + + +class TestModelRepoModelPublish(object): + + @pytest.fixture(params=[(200, 200, 201), (200, 200, 204), (200, 200, 412)]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + payload_list = { + "continuationToken": "null", + "results": [ + { + "comment": "", + "createdOn": "2019-07-09T07:46:06.044161+00:00", + "description": "", + "displayName": "Mxchip 1", + "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"", + "modelName": "example:capabilityModels:Mxchip", + "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", + "publisherName": "microsoft.com", + "type": "CapabilityModel", + "updatedOn": "2019-07-09T21:06:00.072063+00:00", + "urnId": "urn:example:capabilityModels:Mxchip:1", + "version": 1 + } + ] + } + payload_show = { + "@id": "urn:example:capabilityModels:Mxchip:1", + "@type": "CapabilityModel", + "displayName": "Mxchip 1", + "implements": [ + { + "schema":"urn:example:interfaces:MXChip:1", + "name":"MXChip1" + } + ], + "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json" + } + test_side_effect = [ + build_mock_response(mocker, request.param[0], payload=payload_list), + build_mock_response(mocker, request.param[1], payload=payload_show), + build_mock_response(mocker, request.param[2], {}) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) + def test_model_publish(self, fixture_cmd, serviceclient, target_model): + subject.iot_pnp_model_publish(fixture_cmd, model=target_model, + login=mock_target['cs']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + data = args[0][0].data + headers = args[0][0].headers + + assert method == 'PUT' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url + assert json.loads(data)["@id"] == _pnp_generic_model_id + assert headers.get('Authorization') + + @pytest.mark.parametrize("target_model", [('acv.17')]) + def test_model_publish_error(self, fixture_cmd, serviceclient_generic_error, target_model): + with pytest.raises(CLIError): + subject.iot_pnp_model_publish(fixture_cmd, model=target_model, + login=mock_target['cs']) + + +class TestModelRepoModelShow(object): + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + payload = { + "@id": "urn:example:capabilityModels:Mxchip:1", + "@type": "CapabilityModel", + "displayName": "Mxchip 1", + "implements": [ + { + "schema":"urn:example:interfaces:MXChip:1", + "name":"MXChip1" + } + ], + "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json" + } + service_client.return_value = build_mock_response(mocker, request.param, payload=payload) + return service_client + @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) + def test_model_show(self, fixture_cmd, serviceclient, target_model): + result = subject.iot_pnp_model_show(fixture_cmd, + model=target_model, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + headers = args[0][0].headers + + assert method == 'GET' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert json.dumps(result) + assert headers.get('Authorization') + + @pytest.mark.parametrize("target_model", [('acv:17')]) + def test_model_show_error(self, fixture_cmd, serviceclient_generic_error, target_model): + with pytest.raises(CLIError): + subject.iot_pnp_model_show(fixture_cmd, + model=target_model, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + + @pytest.fixture(params=[200]) + def serviceclientemptyresult(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) + def test_model_show_no_result(self, fixture_cmd, serviceclientemptyresult, target_model): + with pytest.raises(CLIError): + subject.iot_pnp_model_show(fixture_cmd, + model=target_model, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + + +class TestModelRepoModelList(object): + @pytest.fixture(params=[200]) + def service_client(self, mocker, fixture_ghcs, request): + serviceclient = mocker.patch(path_service_client) + payload = { + "continuationToken": "null", + "results": [ + { + "comment": "", + "createdOn": "2019-07-09T07:46:06.044161+00:00", + "description": "", + "displayName": "Mxchip 1", + "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"", + "modelName": "example:capabilityModels:Mxchip", + "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", + "publisherName": "microsoft.com", + "type": "CapabilityModel", + "updatedOn": "2019-07-09T21:06:00.072063+00:00", + "urnId": "urn:example:capabilityModels:Mxchip:1", + "version": 1 + } + ] + } + serviceclient.return_value = build_mock_response(mocker, request.param, payload) + return serviceclient + + def test_model_list(self, fixture_cmd, service_client): + result = subject.iot_pnp_model_list(fixture_cmd, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + args = service_client.call_args + url = args[0][0].url + method = args[0][0].method + headers = args[0][0].headers + + assert method == 'POST' + assert '{}/models/search?'.format(_repo_endpoint, _repo_id) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert len(result) == 1 + assert headers.get('Authorization') + + def test_model_list_error(self, fixture_cmd, serviceclient_generic_error): + with pytest.raises(CLIError): + subject.iot_pnp_model_list(fixture_cmd, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + + +class TestModelRepoModelDelete(object): + + @pytest.fixture(params=[204]) + def serviceclient(self, mocker, fixture_ghcs, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) + def test_model_delete(self, fixture_cmd, serviceclient, target_model): + subject.iot_pnp_model_delete(fixture_cmd, + model=target_model, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + headers = args[0][0].headers + + assert method == 'DELETE' + assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url + assert 'repositoryId={}'.format(_repo_id) in url + assert headers.get('Authorization') + + @pytest.mark.parametrize("target_model", [('acv.17')]) + def test_model_delete_error(self, fixture_cmd, serviceclient_generic_error, target_model): + with pytest.raises(CLIError): + subject.iot_pnp_model_delete(fixture_cmd, + model=target_model, + repo_endpoint=mock_target['entity'], + repo_id=mock_target['repository_id']) diff --git a/azext_iot/tests/test_pnp_create_payload_interface.json b/azext_iot/tests/test_pnp_create_payload_interface.json new file mode 100644 index 000000000..422f2deb8 --- /dev/null +++ b/azext_iot/tests/test_pnp_create_payload_interface.json @@ -0,0 +1,210 @@ +{ + "@id": "urn:example:interfaces:MXChip:1", + "@type": "Interface", + "displayName": "MXChip1", + "contents": [ + { + "@type": "Property", + "name": "dieNumber", + "displayName": "Die Number", + "schema": "double" + }, + { + "@type": "Property", + "name": "setCurrent", + "displayName": "Current", + "writable": true, + "schema": "double", + "displayUnit": "amps" + }, + { + "@type": "Property", + "name": "setVoltage", + "displayName": "Voltage", + "writable": true, + "schema": "double", + "displayUnit": "volts" + }, + { + "@type": "Property", + "name": "fanSpeed", + "displayName": "Fan Speed", + "writable": true, + "schema": "double", + "displayUnit": "rpm" + }, + { + "@type": "Property", + "name": "activateIR", + "displayName": "IR", + "writable": true, + "schema": "boolean" + }, + { + "@type": "Telemetry", + "name": "humidity", + "displayName": "Humidity", + "schema": "double", + "displayUnit": "%" + }, + { + "@type": "Telemetry", + "name": "pressure", + "displayName": "Pressure", + "schema": "double", + "displayUnit": "hPa" + }, + { + "@type": "Telemetry", + "name": "magnetometer", + "displayName": "Magnetometer", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "x", + "schema": "double", + "displayUnit": "mgauss" + }, + { + "name": "y", + "schema": "double", + "displayUnit": "mgauss" + }, + { + "name": "z", + "schema": "double", + "displayUnit": "mgauss" + } + ] + } + }, + { + "@type": "Telemetry", + "name": "accelerometer", + "displayName": "Accelerometer", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "x", + "schema": "double", + "displayUnit": "mg" + }, + { + "name": "y", + "schema": "double", + "displayUnit": "mg" + }, + { + "name": "z", + "schema": "double", + "displayUnit": "mg" + } + ] + } + }, + { + "@type": "Telemetry", + "name": "gyroscope", + "displayName": "Gyroscope", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "x", + "schema": "double", + "displayUnit": "mdps" + }, + { + "name": "y", + "schema": "double", + "displayUnit": "mdps" + }, + { + "name": "z", + "schema": "double", + "displayUnit": "mdps" + } + ] + } + }, + { + "@type": "Telemetry", + "name": "buttonBPressed", + "displayName": "Button B Pressed", + "schema": "string" + }, + { + "@type": "Telemetry", + "name": "deviceState", + "displayName": "Device State", + "schema": { + "@type": "Enum", + "valueSchema":"string", + "enumValues": [ + { + "name": "normal", + "displayName": "Normal", + "enumValue": "NORMAL" + }, + { + "name": "danger", + "displayName": "Danger", + "enumValue": "DANGER" + }, + { + "name": "caution", + "displayName": "Caution", + "enumValue": "CAUTION" + } + ] + } + }, + { + "@type": "Command", + "name": "echo", + "displayName": "Echo", + "request": { + "name": "name1", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "displayedValue", + "displayName": "Value to display", + "schema": "string" + } + ] + } + }, + "response": { + "name" : "name2", + "schema": "string" + } + }, + { + "@type": "Command", + "name": "countdown", + "displayName": "Countdown", + "request": { + "name": "name1", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "countFrom", + "displayName": "Count from", + "schema": "double" + } + ] + } + }, + "response": { + "name" : "name3", + "schema": "double" + } + } + ], + "@context": "http://azureiot.com/v1/contexts/Interface.json" +} \ No newline at end of file diff --git a/azext_iot/tests/test_pnp_create_payload_model.json b/azext_iot/tests/test_pnp_create_payload_model.json new file mode 100644 index 000000000..7546052a9 --- /dev/null +++ b/azext_iot/tests/test_pnp_create_payload_model.json @@ -0,0 +1,12 @@ +{ + "@id": "urn:example:capabilityModels:Mxchip:1", + "@type": "CapabilityModel", + "displayName": "Mxchip 1", + "implements": [ + { + "schema":"urn:example:interfaces:MXChip:1", + "name":"MXChip1" + } + ], + "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json" +} \ No newline at end of file diff --git a/azext_iot/tests/test_pnp_interface_list.json b/azext_iot/tests/test_pnp_interface_list.json new file mode 100644 index 000000000..efad99c7b --- /dev/null +++ b/azext_iot/tests/test_pnp_interface_list.json @@ -0,0 +1,19 @@ +{ + "continuationToken": null, + "results": [ + { + "comment": "Requires temperature and humidity sensors.", + "createdOn": "2019-07-03T22:42:21.929126+00:00", + "description": "Provides functionality to report temperature, humidity. Provides telemetry, commands and read-write properties", + "displayName": "Environmental Sensor", + "etag": "\"110043fc-0000-0800-0000-5d27a3960000\"", + "modelName": "contoso:com:EnvironmentalSensor", + "publisherId": "72f988bf-86f1-41af-91ab-2d7cd011db47", + "publisherName": "microsoft.com", + "type": "Interface", + "updatedOn": "2019-07-03T22:42:21.929126+00:00", + "urnId": "urn:contoso:com:EnvironmentalSensor:1", + "version": 1 + } + ] +} \ No newline at end of file diff --git a/azext_iot/tests/test_pnp_interface_show.json b/azext_iot/tests/test_pnp_interface_show.json new file mode 100644 index 000000000..14dae7fbd --- /dev/null +++ b/azext_iot/tests/test_pnp_interface_show.json @@ -0,0 +1,89 @@ +{ + "@context": "http://azureiot.com/v1/contexts/Interface.json", + "@id": "urn:contoso:com:EnvironmentalSensor:1", + "@type": "Interface", + "comment": "Requires temperature and humidity sensors.", + "contents": [ + { + "@type": "Property", + "description": "The state of the device. Two states online/offline are available.", + "displayName": "Device State", + "name": "state", + "schema": "boolean" + }, + { + "@type": "Property", + "description": "The name of the customer currently operating the device.", + "displayName": "Customer Name", + "name": "name", + "schema": "string", + "writable": true + }, + { + "@type": "Property", + "description": "The brightness level for the light on the device. Can be specified as 1 (high), 2 (medium), 3 (low)", + "displayName": "Brightness Level", + "name": "brightness", + "schema": "long", + "writable": true + }, + { + "@type": [ + "Telemetry", + "SemanticType/Temperature" + ], + "description": "Current temperature on the device", + "displayName": "Temperature", + "name": "temp", + "schema": "double", + "unit": "Units/Temperature/fahrenheit" + }, + { + "@type": [ + "Telemetry", + "SemanticType/Humidity" + ], + "description": "Current humidity on the device", + "displayName": "Humidity", + "name": "humid", + "schema": "double", + "unit": "Units/Humidity/percent" + }, + { + "@type": "Command", + "commandType": "synchronous", + "description": "This command will begin blinking the LED for given time interval.", + "name": "blink", + "request": { + "name": "blinkRequest", + "schema": "long" + }, + "response": { + "name": "blinkResponse", + "schema": "string" + } + }, + { + "@type": "Command", + "commandType": "synchronous", + "comment": "This Commands will turn-on the LED light on the device.", + "name": "turnon", + "response": { + "name": "turnonResponse", + "schema": "string" + } + }, + { + "@type": "Command", + "commandType": "synchronous", + "comment": "This Commands will turn-off the LED light on the device.", + "name": "turnoff", + "response": { + "name": "turnoffResponse", + "schema": "string" + } + } + ], + "description": "Provides functionality to report temperature, humidity. Provides telemetry, commands and read-write properties", + "displayName": "Environmental Sensor" + } \ No newline at end of file diff --git a/scripts/ci/test_source.sh b/scripts/ci/test_source.sh index 8595114b8..b088b8019 100644 --- a/scripts/ci/test_source.sh +++ b/scripts/ci/test_source.sh @@ -31,4 +31,10 @@ pytest -v azext_iot/tests/test_iot_dps_unit.py echo "Executing - Utility unit tests" pytest -v azext_iot/tests/test_iot_utility_unit.py +echo "Executing - Pnp unit tests" +pytest -v azext_iot/tests/test_iot_pnp_unit.py + +echo "Executing - Digitaltwin unit tests" +pytest -v azext_iot/tests/test_iot_digitaltwin_unit.py + echo "Tests completed." diff --git a/scripts/ci/test_static.sh b/scripts/ci/test_static.sh index 4aa56fcf0..47f300158 100644 --- a/scripts/ci/test_static.sh +++ b/scripts/ci/test_static.sh @@ -4,5 +4,5 @@ set -e proc_number=`python -c 'import multiprocessing; print(multiprocessing.cpu_count())'` # Run pylint/flake8 on IoT extension -pylint azext_iot/ --ignore=models,service_sdk,device_sdk,custom_sdk,dps_sdk --rcfile=./.pylintrc -j $proc_number +pylint azext_iot/ --ignore=models,service_sdk,device_sdk,custom_sdk,dps_sdk,pnp_sdk --rcfile=./.pylintrc -j $proc_number flake8 --statistics --exclude=*_sdk --append-config=./.flake8 azext_iot/ diff --git a/scripts/ci/test_static_py2.sh b/scripts/ci/test_static_py2.sh index fce599a33..4e8ab57ab 100644 --- a/scripts/ci/test_static_py2.sh +++ b/scripts/ci/test_static_py2.sh @@ -4,5 +4,5 @@ set -e proc_number=`python -c 'import multiprocessing; print(multiprocessing.cpu_count())'` # Run pylint/flake8 on IoT extension -pylint azext_iot/ --ignore=models,service_sdk,device_sdk,custom_sdk,dps_sdk,events3 --rcfile=./.pylintrc -j $proc_number +pylint azext_iot/ --ignore=models,service_sdk,device_sdk,custom_sdk,dps_sdk,pnp_sdk,events3 --rcfile=./.pylintrc -j $proc_number flake8 --statistics --exclude=*_sdk,events3 --append-config=./.flake8 azext_iot/