diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index 45d869c3b..281aa5f11 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -2,9 +2,28 @@ pr: none trigger: none -variables: - pythonVersion: '3.6.x' - architecture: 'x64' +parameters: +- name: pythonVersion + type: string + default: '3.6.x' + values: + - 3.6.x + - 3.9.x +- name: architecture + type: string + default: 'x64' +- name: 'testCentral' + type: boolean + default: true +- name: 'testADT' + type: boolean + default: true +- name: 'testDPS' + type: boolean + default: true +- name: 'testHub' + type: boolean + default: true stages: - stage: 'build' @@ -13,13 +32,13 @@ stages: - job: 'Build_Publish_Azure_IoT_CLI_Extension' pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 inputs: - versionSpec: $(pythonVersion) - architecture: $(architecture) + versionSpec: ${{ parameters.pythonVersion }} + architecture: ${{ parameters.architecture }} - template: templates/setup-ci-machine.yml @@ -27,13 +46,13 @@ stages: - job: 'Build_Publish_Azure_CLI_Test_SDK' pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 inputs: - versionSpec: $(pythonVersion) - architecture: $(architecture) + versionSpec: ${{ parameters.pythonVersion }} + architecture: ${{ parameters.architecture }} - template: templates/setup-ci-machine.yml @@ -45,19 +64,20 @@ stages: steps: - template: templates/setup-dev-test-env.yml parameters: - pythonVersion: $(pythonVersion) - architecture: $(architecture) + pythonVersion: ${{ parameters.pythonVersion }} + architecture: ${{ parameters.architecture }} - template: templates/install-and-record-version.yml - stage: 'test' displayName: 'Run tests' pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'ubuntu-latest' dependsOn: build jobs: - job: 'testCentral' displayName: 'Test IoT Central' + condition: eq('${{ parameters.testCentral }}', true) steps: - template: templates/run-tests.yml parameters: @@ -66,6 +86,7 @@ stages: - job: 'testADT' displayName: 'Test Azure DigitalTwins' + condition: eq('${{ parameters.testADT }}', true) steps: - template: templates/run-tests.yml parameters: @@ -74,6 +95,7 @@ stages: - job: 'testDPS' displayName: 'Test DPS' + condition: eq('${{ parameters.testDPS }}', true) steps: - template: templates/run-tests.yml parameters: @@ -82,6 +104,7 @@ stages: - job: 'testHub' displayName: 'Test IoT Hub' + condition: eq('${{ parameters.testHub }}', true) steps: - template: templates/run-tests.yml parameters: @@ -105,8 +128,8 @@ stages: steps: - template: templates/calculate-code-coverage.yml parameters: - pythonVersion: $(pythonVersion) - architecture: $(architecture) + pythonVersion: ${{ parameters.pythonVersion }} + architecture: ${{ parameters.architecture }} - stage: 'release' displayName: 'Stage GitHub release' diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 6a620155e..e4585386e 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -18,7 +18,7 @@ jobs: - job: 'build_and_publish_azure_iot_cli_ext' pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 @@ -31,7 +31,7 @@ jobs: - job: 'build_and_publish_azure_cli_test_sdk' pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 @@ -45,7 +45,7 @@ jobs: - job: 'run_unit_tests_ubuntu' dependsOn: [ 'build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'ubuntu-latest' strategy: matrix: Python36: @@ -62,8 +62,8 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '$(python.version)' - runUnitTests: 'true' - runIntTests: 'false' + runUnitTests: true + runIntTests: false - job: 'run_unit_tests_macOs' dependsOn: ['build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] @@ -74,8 +74,8 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.8.x' - runUnitTests: 'true' - runIntTests: 'false' + runUnitTests: true + runIntTests: false - template: templates/calculate-code-coverage.yml @@ -93,13 +93,13 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.8.x' - runUnitTests: 'true' - runIntTests: 'false' + runUnitTests: true + runIntTests: false - job: 'run_style_check' dependsOn: ['build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 @@ -124,7 +124,7 @@ jobs: dependsOn: ['build_and_publish_azure_iot_cli_ext'] displayName: 'Evaluate IoT extension command table' pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 diff --git a/.azure-devops/nightly.yml b/.azure-devops/nightly.yml new file mode 100644 index 000000000..34ce2b14d --- /dev/null +++ b/.azure-devops/nightly.yml @@ -0,0 +1,88 @@ +# Run nightly at midnight. +schedules: +- cron: "0 0 * * *" + displayName: Nightly Integration Build + branches: + include: + - dev + +variables: + pythonVersion: '3.6.x' + architecture: 'x64' + +stages: + - stage: 'build' + displayName: 'Build and Publish Artifacts' + jobs: + + - job: 'Build_Publish_Azure_IoT_CLI_Extension' + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: $(pythonVersion) + architecture: $(architecture) + + - template: templates/setup-ci-machine.yml + + - template: templates/build-publish-azure-iot-cli-extension.yml + + - job: 'Build_Publish_Azure_CLI_Test_SDK' + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: $(pythonVersion) + architecture: $(architecture) + + - template: templates/setup-ci-machine.yml + + - template: templates/build-publish-azure-cli-test-sdk.yml + + - job: 'recordVersion' + displayName: 'Install and verify version' + dependsOn: [Build_Publish_Azure_IoT_CLI_Extension, Build_Publish_Azure_CLI_Test_SDK] + steps: + - template: templates/setup-dev-test-env.yml + parameters: + pythonVersion: $(pythonVersion) + architecture: $(architecture) + + - template: templates/install-and-record-version.yml + + - stage: 'test' + displayName: 'Run all tests' + pool: + vmImage: 'ubuntu-latest' + dependsOn: build + jobs: + - job: 'azEdge' + displayName: 'Test against edge AZ CLI' + steps: + - template: templates/nightly-tests.yml + parameters: + azureCLIVersion: 'edge' + - job: 'azMin' + dependsOn: 'azEdge' + displayName: 'Test against minimum supported AZ CLI' + steps: + - template: templates/nightly-tests.yml + parameters: + azureCLIVersion: 'min' + + - stage: 'kpi' + displayName: 'Build KPIs' + dependsOn: [build, test] + jobs: + - job: 'calculateCodeCoverage' + displayName: 'Calculate distributed code coverage' + steps: + - template: templates/calculate-code-coverage.yml + parameters: + pythonVersion: $(pythonVersion) + architecture: $(architecture) + diff --git a/.azure-devops/templates/install-azure-cli-min.yml b/.azure-devops/templates/install-azure-cli-min.yml new file mode 100644 index 000000000..ad25dfbaf --- /dev/null +++ b/.azure-devops/templates/install-azure-cli-min.yml @@ -0,0 +1,14 @@ +steps: +- task: PythonScript@0 + displayName: 'Check minimum supported version of Azure CLI' + inputs: + scriptSource: 'inline' + script: | + import json + with open("$(System.DefaultWorkingDirectory)/azext_iot/azext_metadata.json") as f: + metadata = json.load(f) + version = metadata['azext.minCliCoreVersion'] + print('##vso[task.setvariable variable=min_cli_version]{}'.format(version)) +- bash: | + pip install azure-cli==$(min_cli_version) + displayName: "Install minimum supported CLI version" \ No newline at end of file diff --git a/.azure-devops/templates/nightly-tests.yml b/.azure-devops/templates/nightly-tests.yml new file mode 100644 index 000000000..baaa94e6a --- /dev/null +++ b/.azure-devops/templates/nightly-tests.yml @@ -0,0 +1,55 @@ +parameters: +- name: pythonVersion + type: string + default: '3.6.x' +- name: architecture + type: string + default: 'x64' +- name: azureCLIVersion + type: string + default: released + values: + - min + - released + - edge + +steps: + - template: setup-dev-test-env.yml + parameters: + architecture: ${{ parameters.architecture }} + pythonVersion: ${{ parameters.pythonVersion }} + azureCLIVersion: ${{ parameters.azureCLIVersion }} + + - template: set-testenv-sentinel.yml + + - script: | + pytest -vv azext_iot/tests -k "_unit" --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-unit.xml + displayName: 'All unit tests' + env: + COVERAGE_FILE: .coverage.all + + - task: AzureCLI@2 + continueOnError: true + displayName: 'All integration tests' + inputs: + azureSubscription: az-cli-nightly + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + export COVERAGE_FILE=.coverage.all + pytest -vv azext_iot/tests -k "_int" --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-int.xml + + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: .coverage.all + publishLocation: 'Container' + artifactName: 'coverage' + + - task: PublishTestResults@2 + condition: succeededOrFailed() + displayName: 'Publish Test Results' + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/test-*.xml' + testRunTitle: 'Publish test results for Python ${{ parameters.pythonVersion }} on OS $(Agent.OS)' + searchFolder: '$(System.DefaultWorkingDirectory)' diff --git a/.azure-devops/templates/run-tests.yml b/.azure-devops/templates/run-tests.yml index 91db4af4f..dec6cc9f9 100644 --- a/.azure-devops/templates/run-tests.yml +++ b/.azure-devops/templates/run-tests.yml @@ -1,21 +1,46 @@ parameters: - pythonVersion: '3.6.x' - architecture: 'x64' - runUnitTests: 'false' - runIntTests: 'true' - runWithAzureCliReleased: 'true' - path: 'azext_iot/tests' - name: 'all' +- name: pythonVersion + type: string + default: '3.6.x' +- name: architecture + type: string + default: 'x64' +- name: runUnitTests + type: boolean + default: false +- name: runIntTests + type: boolean + default: true +- name: azureCLIVersion + type: string + default: released + values: + - min + - released + - edge +- name: path + type: string + default: 'azext_iot/tests' +- name: name + type: string + default: 'all' steps: - template: setup-dev-test-env.yml parameters: architecture: ${{ parameters.architecture }} pythonVersion: ${{ parameters.pythonVersion }} - runWithAzureCliReleased: ${{ parameters.runWithAzureCliReleased }} + azureCLIVersion: ${{ parameters.azureCLIVersion }} - template: set-testenv-sentinel.yml + - ${{ if eq(parameters.runUnitTests, 'true') }}: + - script: | + pytest -vv ${{ parameters.path }} -k "_unit" --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-unit-${{ parameters.name }}.xml + displayName: '${{ parameters.name }} unit tests' + env: + COVERAGE_FILE: .coverage.${{ parameters.name }} + - ${{ if eq(parameters.runIntTests, 'true') }}: - task: AzureCLI@2 continueOnError: true @@ -28,13 +53,6 @@ steps: export COVERAGE_FILE=.coverage.${{ parameters.name }} pytest -vv ${{ parameters.path }} -k "_int" --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-int-${{ parameters.name }}.xml - - ${{ if eq(parameters.runUnitTests, 'true') }}: - - script: | - pytest -vv ${{ parameters.path }} -k "_unit" --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-unit-${{ parameters.name }}.xml - displayName: '${{ parameters.name }} unit tests' - env: - COVERAGE_FILE: .coverage.${{ parameters.name }} - - task: PublishBuildArtifacts@1 inputs: pathToPublish: .coverage.${{ parameters.name }} diff --git a/.azure-devops/templates/set-testenv-sentinel.yml b/.azure-devops/templates/set-testenv-sentinel.yml index 94950611e..562e43ed4 100644 --- a/.azure-devops/templates/set-testenv-sentinel.yml +++ b/.azure-devops/templates/set-testenv-sentinel.yml @@ -15,6 +15,7 @@ steps: "azext_iot_testhub": os.environ.get("AZEXT_IOT_TESTHUB", sentinel_value), "azext_iot_testdps": os.environ.get("AZEXT_IOT_TESTDPS", sentinel_value), "azext_iot_teststorageuri": os.environ.get("AZEXT_IOT_TESTSTORAGEURI", sentinel_value), + "azext_iot_identity_teststorageid": os.environ.get("AZEXT_IOT_IDENTITY_TESTSTORAGEID", sentinel_value), "azext_iot_central_app_id": os.environ.get("AZEXT_IOT_CENTRAL_APP_ID", sentinel_value), "azext_dt_ep_eventgrid_topic": os.environ.get("AZEXT_DT_EP_EVENTGRID_TOPIC", sentinel_value), "azext_dt_ep_servicebus_namespace": os.environ.get("AZEXT_DT_EP_SERVICEBUS_NAMESPACE", sentinel_value), @@ -39,6 +40,7 @@ steps: AZEXT_IOT_TESTHUB: $(azext_iot_testhub) AZEXT_IOT_TESTDPS: $(azext_iot_testdps) AZEXT_IOT_TESTSTORAGEURI: $(azext_iot_teststorageuri) + AZEXT_IOT_IDENTITY_TESTSTORAGEID: $(azext_iot_identity_teststorageid) AZEXT_IOT_CENTRAL_APP_ID: $(azext_iot_central_app_id) AZEXT_DT_EP_EVENTGRID_TOPIC: $(azext_dt_ep_eventgrid_topic) AZEXT_DT_EP_SERVICEBUS_NAMESPACE: $(azext_dt_ep_servicebus_namespace) diff --git a/.azure-devops/templates/setup-dev-test-env.yml b/.azure-devops/templates/setup-dev-test-env.yml index cc6c1a875..029613517 100644 --- a/.azure-devops/templates/setup-dev-test-env.yml +++ b/.azure-devops/templates/setup-dev-test-env.yml @@ -1,7 +1,17 @@ parameters: - pythonVersion: '' - architecture: '' - runWithAzureCliReleased: 'true' +- name: pythonVersion + type: string + default: '3.6.x' +- name: architecture + type: string + default: 'x64' +- name: azureCLIVersion + type: string + default: 'released' + values: + - min + - released + - edge steps: - task: UsePythonVersion@0 @@ -9,12 +19,15 @@ steps: versionSpec: ${{ parameters.pythonVersion }} architecture: ${{ parameters.architecture }} - - ${{ if eq(parameters.runWithAzureCliReleased, 'false') }}: - - template: install-azure-cli-edge.yml + - ${{ if eq(parameters.azureCLIVersion, 'min') }}: + - template: install-azure-cli-min.yml - - ${{ if eq(parameters.runWithAzureCliReleased, 'true') }}: + - ${{ if eq(parameters.azureCLIVersion, 'released') }}: - template: install-azure-cli-released.yml + - ${{ if eq(parameters.azureCLIVersion, 'edge') }}: + - template: install-azure-cli-edge.yml + - template: download-install-local-azure-test-sdk.yml - template: setup-ci-machine.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 78122037e..ebd3bb01c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,22 +5,11 @@ * @digimaun # Central Code Owner(s) -azext_iot/central/ @prbans -azext_iot/tests/central/ @prbans -azext_iot/tests/test_iot_central_int.py @prbans -azext_iot/tests/test_iot_central_unit.py @prbans - -# Monitor Code Owner(s) -azext_iot/monitor/ @prbans @digimaun +azext_iot/central/ @valluriraj +azext_iot/tests/central/ @valluriraj +azext_iot/tests/test_iot_central_int.py @valluriraj +azext_iot/tests/test_iot_central_unit.py @valluriraj # AICS Code Owner(s) azext_iot/product/ @montgomp @c-ryan-k azext_iot/tests/product/ @montgomp @c-ryan-k - -# PnP Repository Code Owners(s) -azext_iot/pnp/ @c-ryan-k -azext_iot/tests/pnp/ @c-ryan-k - -# Test Code Owners -azext_iot/tests/test_monitor_parsers_unit.py @prbans @digimaun -azext_iot/tests/test_uamqp_import.py @prbans @digimaun diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b199d06c..8301c0439 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -140,13 +140,13 @@ You can either manually set the environment variables or use the `pytest.ini.exa Execute the following command to run the IoT Hub integration tests: -`pytest azext_iot/tests/iothub/test_iot_ext_int.py` +`pytest azext_iot/tests/iothub/ -k "_int"` ##### Device Provisioning Service Execute the following command to run the IoT Hub DPS integration tests: -`pytest azext_iot/tests/dps/test_iot_dps_int.py` +`pytest azext_iot/tests/dps/ -k "_int"` #### Unit and Integration Tests Single Command diff --git a/HISTORY.rst b/HISTORY.rst index 2dd511e21..1f827555e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,74 @@ Release History =============== +0.10.14 ++++++++++++++++ + +**IoT Hub updates** + +* Fix for "az iot hub c2d-message receive" - the command will use the "ContentEncoding" header value (which indicates the message body encoding) + or fallback to utf-8 to decode the received message body. + +**Azure Digital Twins updates** + +* Addition of the following commands + + * az dt reset - Preview command which deletes all data entities from the target instance (models, twins, twin relationships). + + +0.10.13 ++++++++++++++++ + +**General updates** + +* Min CLI core version raised to 2.17.1 + + +0.10.12 ++++++++++++++++ + +**IoT Central updates** + +* Public API GA update + + * Remove preview tag for api-token, device, device-template, user routes. Default routes use central GA API's. + * Add support for preview and 1.0 routes. + * Addition of the optional '--av' argument to specify the version of API for the requested operation. + +**IoT Hub updates** + +* Removed deprecated edge offline commands and artifacts. +* Removed deprecated device-identity | module-identity show-connection-string commands. + +* Most commands against IoT Hub support Azure AD based access. The type of auth + used to execute commands can be controlled with the "--auth-type" parameter + which accepts the values "key" or "login". The value of "key" is set by default. + + * When "--auth-type" has the value of "key", like before the CLI will auto-discover + a suitable policy when interacting with iothub. + * When "--auth-type" has the value "login", an access token from the Azure CLI logged in principal + will be used for the operation. + + * The following commands currently remain with key based access only. + + * az iot hub monitor-events + * az iot device c2d-message receive + * az iot device c2d-message complete + * az iot device c2d-message abandon + * az iot device c2d-message reject + * az iot device c2d-message purge + * az iot device send-d2c-message + * az iot device simulate + +For more information about IoT Hub support for AAD visit: https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-dev-guide-azure-ad-rbac + +**Azure Digital Twins updates** + +* Addition of the following commands + + * az dt model delete-all - Deletes all models associated with the Digital Twins instance. + + 0.10.11 +++++++++++++++ diff --git a/README.md b/README.md index 8378708a6..6dc128549 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,25 @@ The **Azure IoT extension for Azure CLI** aims to accelerate the development, management and automation of Azure IoT solutions. It does this via addition of rich features and functionality to the official [Azure CLI](https://docs.microsoft.com/en-us/cli/azure). ## News +- Starting with version `0.10.13` of the IoT extension, you will need an Azure CLI core version of `2.17.1` or higher. IoT extension version `0.10.11` remains on the extension index to support environments that cannot upgrade core CLI versions. -The legacy IoT extension Id `azure-cli-iot-ext` is deprecated in favor of the new modern Id `azure-iot`. `azure-iot` is a superset of `azure-cli-iot-ext` and any new features or fixes will apply to `azure-iot` only. Also the legacy and modern IoT extension should **never** co-exist in the same CLI environment. +- Azure CLI `2.24.0` requires an `azure-iot` extension update to `0.10.11` or later for IoT Hub commands to work properly. This can be done with `az extension update --name azure-iot`. A common error that arises when using an older `azure-iot` with Azure CLI `2.24.0` looks like `AttributeError: 'IotHubResourceOperations' object has no attribute 'config'`. -Uninstall the legacy extension with the following command: `az extension remove --name azure-cli-iot-ext`. +- The legacy IoT extension Id `azure-cli-iot-ext` is deprecated in favor of the new modern Id `azure-iot`. `azure-iot` is a superset of `azure-cli-iot-ext` and any new features or fixes will apply to `azure-iot` only. Also the legacy and modern IoT extension should **never** co-exist in the same CLI environment. -Related - if you see an error with a stacktrace similar to: -``` -... -azure-cli-iot-ext/azext_iot/common/_azure.py, ln 90, in get_iot_hub_connection_string - client = iot_hub_service_factory(cmd.cli_ctx) -cliextensions/azure-cli-iot-ext/azext_iot/_factory.py, ln 29, in iot_hub_service_factory - from azure.mgmt.iothub.iot_hub_client import IotHubClient -ModuleNotFoundError: No module named 'azure.mgmt.iothub.iot_hub_client' -``` + Uninstall the legacy extension with the following command: `az extension remove --name azure-cli-iot-ext`. + + Related - if you see an error with a stacktrace similar to: + ``` + ... + azure-cli-iot-ext/azext_iot/common/_azure.py, ln 90, in get_iot_hub_connection_string + client = iot_hub_service_factory(cmd.cli_ctx) + cliextensions/azure-cli-iot-ext/azext_iot/_factory.py, ln 29, in iot_hub_service_factory + from azure.mgmt.iothub.iot_hub_client import IotHubClient + ModuleNotFoundError: No module named 'azure.mgmt.iothub.iot_hub_client' + ``` -The resolution is to remove the deprecated `azure-cli-iot-ext` and install any version of the `azure-iot` extension. + The resolution is to remove the deprecated `azure-cli-iot-ext` and install any version of the `azure-iot` extension. ## Commands diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py index 98fcbbf44..aee601e82 100644 --- a/azext_iot/_factory.py +++ b/azext_iot/_factory.py @@ -9,8 +9,9 @@ """ from azext_iot.common.sas_token_auth import SasTokenAuthentication -from azext_iot.common.shared import SdkType -from azext_iot.constants import USER_AGENT +from azext_iot.iothub.providers.aad_oauth import IoTHubOAuth +from azext_iot.common.shared import SdkType, AuthenticationTypeDataplane +from azext_iot.constants import USER_AGENT, IOTHUB_RESOURCE_ID from msrestazure.azure_exceptions import CloudError __all__ = [ @@ -97,15 +98,21 @@ def _get_iothub_device_sdk(self): def _get_iothub_service_sdk(self): from azext_iot.sdk.iothub.service import IotHubGatewayServiceAPIs - credentials = ( - self.auth_override - if self.auth_override - else SasTokenAuthentication( + credentials = None + + if self.auth_override: + credentials = self.auth_override + elif self.target["policy"] == AuthenticationTypeDataplane.login.value: + credentials = IoTHubOAuth( + cmd=self.target["cmd"], + resource_id=IOTHUB_RESOURCE_ID + ) + else: + credentials = SasTokenAuthentication( uri=self.sas_uri, shared_access_policy_name=self.target["policy"], shared_access_key=self.target["primarykey"], ) - ) return IotHubGatewayServiceAPIs(credentials=credentials, base_url=self.endpoint) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 250f0a693..f2c025eeb 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -145,14 +145,6 @@ - name: Create an edge enabled IoT device with default authorization (shared private key). text: > az iot hub device-identity create -n {iothub_name} -d {device_id} --ee - - name: Create an edge enabled IoT device with default authorization (shared private key) and - add child devices as well. - text: > - az iot hub device-identity create -n {iothub_name} -d {device_id} --ee --cl {child_device_id} - - name: Create an IoT device with default authorization (shared private key) and - set parent device as well. - text: > - az iot hub device-identity create -n {iothub_name} -d {device_id} --pd {edge_device_id} - name: Create an IoT device with self-signed certificate authorization, generate a cert valid for 10 days then use its thumbprint. text: > @@ -234,13 +226,6 @@ short-summary: Delete an IoT Hub device. """ -helps[ - "iot hub device-identity show-connection-string" -] = """ - type: command - short-summary: Show a given IoT Hub device connection string. -""" - helps[ "iot hub device-identity connection-string" ] = """ @@ -271,6 +256,17 @@ - name: Export all device identities to a configured blob container using a file path which contains the SAS uri. text: > az iot hub device-identity export -n {iothub_name} --bcu {sas_uri_filepath} + - name: Export all device identities to a configured blob container and include device keys. Uses system assigned identity that has + Storage Blob Data Contributor roles for the storage account. The blob container uri does not need the blob SAS token. + text: > + az iot hub device-identity export -n {iothub_name} --ik --bcu + 'https://mystorageaccount.blob.core.windows.net/devices' --auth-type identity --identity [system] + - name: Export all device identities to a configured blob container and include device keys. Uses user assigned managed identity + that has Storage Blob Data Contributor roles for the storage account and contributor for the IoT hub. The blob container + uri does not need the blob SAS token. + text: > + az iot hub device-identity export -n {iothub_name} --ik --bcu + 'https://mystorageaccount.blob.core.windows.net/devices' --auth-type identity --identity {managed_identity_resource_id} """ helps[ @@ -288,32 +284,15 @@ - name: Import all device identities from a blob using a file path which contains SAS uri. text: > az iot hub device-identity import -n {iothub_name} --ibcu {input_sas_uri_filepath} --obcu {output_sas_uri_filepath} -""" - -helps[ - "iot hub device-identity get-parent" -] = """ - type: command - short-summary: Get the parent device of the specified device. - examples: - - name: Get the parent device of the specified device. - text: > - az iot hub device-identity get-parent -d {device_id} -n {iothub_name} -""" - -helps[ - "iot hub device-identity set-parent" -] = """ - type: command - short-summary: Set the parent device of the specified device. - examples: - - name: Set the parent device of the specified device. + - name: Import all device identities from a blob using system assigned identity that has Storage Blob Data Contributor + roles for both storage accounts. The blob container uri does not need the blob SAS token. text: > - az iot hub device-identity set-parent -d {device_id} --pd {edge_device_id} -n {iothub_name} - - name: Set the parent device of the specified device irrespectively the device is - already a child of other edge device. + az iot hub device-identity import -n {iothub_name} --ibcu {input_sas_uri} --obcu {output_sas_uri} --auth-type identity --identity [system] + - name: Import all device identities from a blob using user assigned managed identity that has Storage Blob Data Contributor + roles for both storage accounts and contributor for the IoT hub. The blob container uri does not need the blob SAS token. text: > - az iot hub device-identity set-parent -d {device_id} --pd {edge_device_id} --force -n {iothub_name} + az iot hub device-identity import -n {iothub_name} --ibcu {input_sas_uri} --obcu {output_sas_uri} + --auth-type identity --identity {managed_identity_resource_id} """ helps[ @@ -348,49 +327,6 @@ az iot hub device-identity parent set -d {device_id} --pd {edge_device_id} --force -n {iothub_name} """ -helps[ - "iot hub device-identity add-children" -] = """ - type: command - short-summary: Add specified comma-separated list of device ids as children of specified edge device. - examples: - - name: Add devices as a children to the edge device. - text: > - az iot hub device-identity add-children -d {edge_device_id} --child-list {comma_separated_device_id} - -n {iothub_name} - - name: Add devices as a children to the edge device irrespectively the device is - already a child of other edge device. - text: > - az iot hub device-identity add-children -d {edge_device_id} --child-list {comma_separated_device_id} - -n {iothub_name} -f -""" - -helps[ - "iot hub device-identity list-children" -] = """ - type: command - short-summary: Outputs comma-separated list of assigned child devices. - examples: - - name: Show all assigned devices as comma-separated list. - text: > - az iot hub device-identity list-children -d {edge_device_id} -n {iothub_name} -""" - -helps[ - "iot hub device-identity remove-children" -] = """ - type: command - short-summary: Remove devices as children from specified edge device. - examples: - - name: Remove all mentioned devices as children of specified device. - text: > - az iot hub device-identity remove-children -d {edge_device_id} --child-list {comma_separated_device_id} - -n {iothub_name} - - name: Remove all devices as children specified edge device. - text: > - az iot hub device-identity remove-children -d {edge_device_id} --remove-all -""" - helps[ "iot hub device-identity children" ] = """ @@ -500,13 +436,6 @@ short-summary: Manage IoT device modules. """ -helps[ - "iot hub module-identity show-connection-string" -] = """ - type: command - short-summary: Show a target IoT device module connection string. -""" - helps[ "iot hub module-identity create" ] = """ @@ -543,6 +472,18 @@ authentication.symmetricKey.secondaryKey="" """ +helps[ + "iot hub module-identity renew-key" +] = """ + type: command + short-summary: Renew target keys of an IoT Hub device module with sas authentication. + examples: + - name: Renew the primary key. + text: az iot hub module-identity renew-key -m {module_name} -d {device_id} -n {iothub_name} --kt primary + - name: Swap the primary and secondary keys. + text: az iot hub module-identity renew-key -m {module_name} -d {device_id} -n {iothub_name} --kt swap +""" + helps[ "iot hub module-identity delete" ] = """ @@ -899,8 +840,11 @@ "iot device send-d2c-message" ] = """ type: command - short-summary: Send an mqtt device-to-cloud message. - The command supports sending messages with application and system properties. + short-summary: | + Send an mqtt device-to-cloud message. + The command supports sending messages with application and system properties. + + Note: The command only works for symmetric key auth (SAS) based devices examples: - name: Basic usage text: az iot device send-d2c-message -n {iothub_name} -d {device_id} @@ -922,7 +866,8 @@ While the device simulation is running, the device will automatically receive and acknowledge cloud-to-device (c2d) messages. For mqtt simulation, all c2d messages will be acknowledged with completion. For http simulation c2d acknowledgement is based on user - selection which can be complete, reject or abandon. The mqtt simulation also supports direct + selection which can be complete, reject or abandon. Additionally, mqtt simulation is only + supported for symmetric key auth (SAS) based devices. The mqtt simulation also supports direct method invocation which can be acknowledged by a response status code and response payload Note: The command by default will set content-type to application/json and content-encoding diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 18d205789..d9f1e4f68 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -31,10 +31,29 @@ JobCreateType, JobStatusType, AuthenticationType, + AuthenticationTypeDataplane, RenewKeyType, ) from azext_iot._validators import mode2_iot_login_handler from azext_iot.assets.user_messages import info_param_properties_device +from azure.cli.core.local_context import LocalContextAttribute, LocalContextAction + + +auth_type_dataplane_param_type = CLIArgumentType( + options_list=["--auth-type"], + arg_type=get_enum_type( + AuthenticationTypeDataplane, AuthenticationTypeDataplane.key.value + ), + arg_group="Access Control", + help="Indicates whether the operation should auto-derive a policy key or use the current Azure AD session. " + "You can configure the default using `az configure --defaults iothub-data-auth-type=`", + configured_default="iothub-data-auth-type", + local_context_attribute=LocalContextAttribute( + name="iothub-data-auth-type", + actions=[LocalContextAction.SET, LocalContextAction.GET], + scopes=["iot"], + ), +) hub_name_type = CLIArgumentType( completer=get_resource_name_completion_list("Microsoft.Devices/IotHubs"), @@ -106,7 +125,7 @@ def load_arguments(self, _): "etag", options_list=["--etag", "-e"], help="Etag or entity tag corresponding to the last state of the resource. " - "If no etag is provided the value '*' is used." + "If no etag is provided the value '*' is used.", ) context.argument( "top", @@ -250,6 +269,11 @@ def load_arguments(self, _): options_list=["--desired"], help="Twin desired properties.", ) + context.argument( + "auth_type_dataplane", + options_list=["--auth-type"], + arg_type=auth_type_dataplane_param_type, + ) with self.argument_context("iot hub connection-string") as context: context.argument( @@ -359,7 +383,7 @@ def load_arguments(self, _): help="Description for device status.", ) - with self.argument_context('iot hub device-identity update') as context: + with self.argument_context("iot hub device-identity update") as context: context.argument( "primary_key", options_list=["--primary-key", "--pk"], @@ -371,39 +395,12 @@ def load_arguments(self, _): help="The secondary symmetric shared access key stored in base64 format.", ) - with self.argument_context("iot hub device-identity create") as context: - context.argument( - "force", - options_list=["--force", "-f"], - arg_type=get_three_state_flag(), - help="Overwrites the device's parent device. " - "This command parameter has been deprecated and will be removed " - "in a future release. Use 'az iot hub device-identity parent set' instead.", - deprecate_info=context.deprecate() - ) - context.argument( - "set_parent_id", - options_list=["--set-parent", "--pd"], - help="Id of edge device. " - "This command parameter has been deprecated and will be removed " - "in a future release. Use 'az iot hub device-identity parent set' instead.", - deprecate_info=context.deprecate() - ) - context.argument( - "add_children", - options_list=["--add-children", "--cl"], - help="Child device list (comma separated). " - "This command parameter has been deprecated and will be removed " - "in a future release. Use 'az iot hub device-identity children add' instead.", - deprecate_info=context.deprecate() - ) - - with self.argument_context('iot hub device-identity renew-key') as context: + with self.argument_context("iot hub device-identity renew-key") as context: context.argument( "renew_key_type", options_list=["--key-type", "--kt"], arg_type=get_enum_type(RenewKeyType), - help="Target key type to regenerate." + help="Target key type to regenerate.", ) with self.argument_context("iot hub device-identity export") as context: @@ -413,7 +410,8 @@ def load_arguments(self, _): help="Blob Shared Access Signature URI with write, read, and delete access to " "a blob container. This is used to output the status of the " "job and the results. Note: when using Identity-based authentication an " - "https:// URI is still required. Input for this argument can be inline or from a file path.", + "https:// URI is still required - but no SAS token is necessary. Input for this argument " + "can be inline or from a file path.", ) context.argument( "include_keys", @@ -428,6 +426,15 @@ def load_arguments(self, _): arg_type=get_enum_type(AuthenticationType), help="Authentication type for communicating with the storage container.", ) + context.argument( + "identity", + options_list=["--identity"], + help="Managed identity type to determine if system assigned managed identity or " + "user assigned managed identity is used. For system assigned managed identity, use " + "[system]. For user assigned managed identity, provide the user assigned managed " + "identity resource id. This identity requires a Storage Blob Data Contributor roles for the Storage " + "Account.", + ) with self.argument_context("iot hub device-identity import") as context: context.argument( @@ -436,8 +443,8 @@ def load_arguments(self, _): help="Blob Shared Access Signature URI with read access to a blob " "container. This blob contains the operations to be performed on " "the identity registry. Note: when using Identity-based authentication " - "an https:// URI is still required. Input for this argument can be inline " - "or from a file path.", + "an https:// URI is still required - but no SAS token is necessary. Input for this " + "argument can be inline or from a file path.", ) context.argument( "output_blob_container_uri", @@ -445,8 +452,8 @@ def load_arguments(self, _): help="Blob Shared Access Signature URI with write access " "to a blob container. This is used to output the status of " "the job and the results. Note: when using Identity-based " - "authentication an https:// URI is still required. Input for " - "this argument can be inline or from a file path.", + "authentication an https:// URI without the SAS token is still required. " + "Input for this argument can be inline or from a file path.", ) context.argument( "storage_authentication_type", @@ -454,52 +461,16 @@ def load_arguments(self, _): arg_type=get_enum_type(AuthenticationType), help="Authentication type for communicating with the storage container.", ) - - with self.argument_context("iot hub device-identity get-parent") as context: - context.argument("device_id", help="Id of device.") - - with self.argument_context("iot hub device-identity set-parent") as context: - context.argument("device_id", help="Id of device.") - context.argument( - "parent_id", - options_list=["--parent-device-id", "--pd"], - help="Id of edge device.", - ) context.argument( - "force", - options_list=["--force", "-f"], - help="Overwrites the device's parent device.", + "identity", + options_list=["--identity"], + help="Managed identity type to determine if system assigned managed identity or " + "user assigned managed identity is used. For system assigned managed identity, use " + "[system]. For user assigned managed identity, provide the user assigned managed " + "identity resource id. This identity requires a Storage Blob Data Contributor role for the target Storage " + "Account and Contributor role for the IoT Hub.", ) - with self.argument_context("iot hub device-identity add-children") as context: - context.argument("device_id", help="Id of edge device.") - context.argument( - "child_list", - options_list=["--child-list", "--cl"], - help="Child device list (comma separated).", - ) - context.argument( - "force", - options_list=["--force", "-f"], - help="Overwrites the child device's parent device.", - ) - - with self.argument_context("iot hub device-identity remove-children") as context: - context.argument("device_id", help="Id of edge device.") - context.argument( - "child_list", - options_list=["--child-list", "--cl"], - help="Child device list (comma separated).", - ) - context.argument( - "remove_all", - options_list=["--remove-all", "-a"], - help="To remove all children.", - ) - - with self.argument_context("iot hub device-identity list-children") as context: - context.argument("device_id", help="Id of edge device.") - with self.argument_context("iot hub device-identity parent set") as context: context.argument( "parent_id", @@ -530,6 +501,14 @@ def load_arguments(self, _): help="To remove all children.", ) + with self.argument_context("iot hub module-identity renew-key") as context: + context.argument( + "renew_key_type", + options_list=["--key-type", "--kt"], + arg_type=get_enum_type(RenewKeyType), + help="Target key type to regenerate.", + ) + with self.argument_context("iot hub distributed-tracing update") as context: context.argument( "sampling_mode", @@ -558,6 +537,11 @@ def load_arguments(self, _): ) with self.argument_context("iot device") as context: + context.argument( + "auth_type_dataplane", + options_list=["--auth-type"], + arg_type=auth_type_dataplane_param_type, + ) context.argument("data", options_list=["--data", "--da"], help="Message body.") context.argument( "properties", @@ -646,12 +630,12 @@ def load_arguments(self, _): context.argument( "content_type", options_list=["--content-type", "--ct"], - help="The content type associated with the C2D message.", + help="The content type for the C2D message body.", ) context.argument( "content_encoding", options_list=["--content-encoding", "--ce"], - help="The content encoding associated with the C2D message.", + help="The encoding for the C2D message body.", ) with self.argument_context("iot device c2d-message send") as context: @@ -800,6 +784,11 @@ def load_arguments(self, _): arg_type=get_three_state_flag(), help="Disables client side schema validation for edge deployment creation.", ) + context.argument( + "auth_type_dataplane", + options_list=["--auth-type"], + arg_type=auth_type_dataplane_param_type, + ) with self.argument_context("iot dps") as context: context.argument( @@ -932,7 +921,7 @@ def load_arguments(self, _): "show_keys", options_list=["--show-keys", "--keys"], arg_type=get_three_state_flag(), - help="Include attestation keys and information in enrollment results" + help="Include attestation keys and information in enrollment results", ) with self.argument_context("iot dps enrollment update") as context: @@ -984,7 +973,7 @@ def load_arguments(self, _): "show_keys", options_list=["--show-keys", "--keys"], arg_type=get_three_state_flag(), - help="Include attestation keys and information in enrollment group results" + help="Include attestation keys and information in enrollment group results", ) with self.argument_context("iot dps registration") as context: diff --git a/azext_iot/azext_metadata.json b/azext_iot/azext_metadata.json index 5bed66c74..761a3e110 100644 --- a/azext_iot/azext_metadata.json +++ b/azext_iot/azext_metadata.json @@ -1,3 +1,3 @@ { - "azext.minCliCoreVersion": "2.3.1" + "azext.minCliCoreVersion": "2.17.1" } diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index bdde96408..35c6d18e1 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -67,7 +67,7 @@ def _load_central_devices_help(): az iot central device create --app-id {appid} --device-id {deviceid} - --instance-of {devicetemplateid} + --template {devicetemplateid} --simulated """ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index fba1e64f6..0c97daca9 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -60,7 +60,7 @@ def load_central_commands(self, _): ) with self.command_group( - "iot central user", command_type=central_user_ops, is_preview=True, + "iot central user", command_type=central_user_ops, ) as cmd_group: cmd_group.command("create", "add_user") cmd_group.command("list", "list_users") @@ -68,7 +68,7 @@ def load_central_commands(self, _): cmd_group.command("delete", "delete_user") with self.command_group( - "iot central api-token", command_type=central_api_token_ops, is_preview=True, + "iot central api-token", command_type=central_api_token_ops, ) as cmd_group: cmd_group.command("create", "add_api_token") cmd_group.command("list", "list_api_tokens") @@ -76,7 +76,7 @@ def load_central_commands(self, _): cmd_group.command("delete", "delete_api_token") with self.command_group( - "iot central device", command_type=central_device_ops, is_preview=True, + "iot central device", command_type=central_device_ops, ) as cmd_group: # cmd_group.command("list", "list_devices") cmd_group.show_command("show", "get_device") @@ -89,15 +89,13 @@ def load_central_commands(self, _): cmd_group.command("manual-failback", "run_manual_failback") with self.command_group( - "iot central device command", command_type=central_device_ops, is_preview=True, + "iot central device command", command_type=central_device_ops, ) as cmd_group: cmd_group.command("run", "run_command") cmd_group.command("history", "get_command_history") with self.command_group( - "iot central device-template", - command_type=central_device_templates_ops, - is_preview=True, + "iot central device-template", command_type=central_device_templates_ops, ) as cmd_group: # cmd_group.command("list", "list_device_templates") # cmd_group.command("map", "map_device_templates") @@ -106,7 +104,7 @@ def load_central_commands(self, _): cmd_group.command("delete", "delete_device_template") with self.command_group( - "iot central device twin", command_type=central_device_twin_ops, is_preview=True + "iot central device twin", command_type=central_device_twin_ops, ) as cmd_group: cmd_group.show_command( "show", "device_twin_show", diff --git a/azext_iot/central/commands_api_token.py b/azext_iot/central/commands_api_token.py index e8111f1a6..6a7f3cd30 100644 --- a/azext_iot/central/commands_api_token.py +++ b/azext_iot/central/commands_api_token.py @@ -7,8 +7,9 @@ from azext_iot.constants import CENTRAL_ENDPOINT -from azext_iot.central.providers import CentralApiTokenProvider -from azext_iot.central.models.enum import Role +from azext_iot.central.providers.preview import CentralApiTokenProviderPreview +from azext_iot.central.providers.v1 import CentralApiTokenProviderV1 +from azext_iot.central.models.enum import Role, ApiVersion def add_api_token( @@ -18,36 +19,66 @@ def add_api_token( role: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralApiTokenProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralApiTokenProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralApiTokenProviderV1(cmd=cmd, app_id=app_id, token=token) + return provider.add_api_token( token_id=token_id, role=Role[role], central_dns_suffix=central_dns_suffix, ) def list_api_tokens( - cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralApiTokenProvider(cmd=cmd, app_id=app_id, token=token) - return provider.get_api_token_list(central_dns_suffix=central_dns_suffix,) + if api_version == ApiVersion.preview.value: + provider = CentralApiTokenProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralApiTokenProviderV1(cmd=cmd, app_id=app_id, token=token) + + return provider.get_api_token_list(central_dns_suffix=central_dns_suffix) def get_api_token( - cmd, app_id: str, token_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + token_id: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralApiTokenProvider(cmd=cmd, app_id=app_id, token=token) + + if api_version == ApiVersion.preview.value: + provider = CentralApiTokenProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralApiTokenProviderV1(cmd=cmd, app_id=app_id, token=token) return provider.get_api_token( - token_id=token_id, central_dns_suffix=central_dns_suffix + token_id=token_id, central_dns_suffix=central_dns_suffix, ) def delete_api_token( - cmd, app_id: str, token_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + token_id: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralApiTokenProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralApiTokenProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralApiTokenProviderV1(cmd=cmd, app_id=app_id, token=token) return provider.delete_api_token( - token_id=token_id, central_dns_suffix=central_dns_suffix + token_id=token_id, central_dns_suffix=central_dns_suffix, ) diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py index 38b80e8d8..df254e2a6 100644 --- a/azext_iot/central/commands_device.py +++ b/azext_iot/central/commands_device.py @@ -9,18 +9,39 @@ from azext_iot.common import utility from azext_iot.constants import CENTRAL_ENDPOINT -from azext_iot.central.providers import CentralDeviceProvider +from azext_iot.central.providers.preview import CentralDeviceProviderPreview +from azext_iot.central.providers.v1 import CentralDeviceProviderV1 +from azext_iot.central.models.enum import ApiVersion -def list_devices(cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT): - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) +def list_devices( + cmd, + app_id: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, +): + if api_version == ApiVersion.preview.value: + provider = CentralDeviceProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token) + return provider.list_devices(central_dns_suffix=central_dns_suffix) def get_device( - cmd, app_id: str, device_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + device_id: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralDeviceProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token) + return provider.get_device(device_id, central_dns_suffix=central_dns_suffix) @@ -29,38 +50,51 @@ def create_device( app_id: str, device_id: str, device_name=None, - instance_of=None, + template=None, simulated=False, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - if simulated and not instance_of: + if simulated and not template: raise CLIError( - "Error: if you supply --simulated you must also specify --instance-of" + "Error: if you supply --simulated you must also specify --template" ) - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) + + if api_version == ApiVersion.preview.value: + provider = CentralDeviceProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token) + return provider.create_device( device_id=device_id, device_name=device_name, - instance_of=instance_of, + template=template, simulated=simulated, central_dns_suffix=central_dns_suffix, ) def delete_device( - cmd, app_id: str, device_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + device_id: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) - return provider.delete_device( - device_id=device_id, - central_dns_suffix=central_dns_suffix) + if api_version == ApiVersion.preview.value: + provider = CentralDeviceProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token) + + return provider.delete_device(device_id, central_dns_suffix=central_dns_suffix) def registration_info( cmd, app_id: str, device_id, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token,) + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token) return provider.get_device_registration_info( device_id=device_id, central_dns_suffix=central_dns_suffix, device_status=None, @@ -76,18 +110,24 @@ def run_command( content: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): if not isinstance(content, str): raise CLIError("content must be a string: {}".format(content)) payload = utility.process_json_arg(content, argument_name="content") - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralDeviceProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token) + return provider.run_component_command( device_id=device_id, interface_id=interface_id, command_name=command_name, payload=payload, + central_dns_suffix=central_dns_suffix, ) @@ -102,25 +142,20 @@ def run_manual_failover( if ttl_minutes and ttl_minutes < 1: raise CLIError("TTL value should be a positive integer: {}".format(ttl_minutes)) - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token) return provider.run_manual_failover( device_id=device_id, ttl_minutes=ttl_minutes, - central_dns_suffix=central_dns_suffix + central_dns_suffix=central_dns_suffix, ) def run_manual_failback( - cmd, - app_id: str, - device_id: str, - token=None, - central_dns_suffix=CENTRAL_ENDPOINT, + cmd, app_id: str, device_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token) return provider.run_manual_failback( - device_id=device_id, - central_dns_suffix=central_dns_suffix + device_id=device_id, central_dns_suffix=central_dns_suffix ) @@ -132,17 +167,25 @@ def get_command_history( command_name: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralDeviceProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token) + return provider.get_component_command_history( - device_id=device_id, interface_id=interface_id, command_name=command_name, + device_id=device_id, + interface_id=interface_id, + command_name=command_name, + central_dns_suffix=central_dns_suffix, ) def registration_summary( cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token,) + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token,) return provider.get_device_registration_summary( central_dns_suffix=central_dns_suffix, ) @@ -151,7 +194,7 @@ def registration_summary( def get_credentials( cmd, app_id: str, device_id, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token,) + provider = CentralDeviceProviderV1(cmd=cmd, app_id=app_id, token=token,) return provider.get_device_credentials( device_id=device_id, central_dns_suffix=central_dns_suffix, ) diff --git a/azext_iot/central/commands_device_template.py b/azext_iot/central/commands_device_template.py index 9cbcdfab9..fd6d90e63 100644 --- a/azext_iot/central/commands_device_template.py +++ b/azext_iot/central/commands_device_template.py @@ -9,7 +9,9 @@ from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.common import utility -from azext_iot.central.providers import CentralDeviceTemplateProvider +from azext_iot.central.providers.preview import CentralDeviceTemplateProviderPreview +from azext_iot.central.providers.v1 import CentralDeviceTemplateProviderV1 +from azext_iot.central.models.enum import ApiVersion def get_device_template( @@ -18,26 +20,53 @@ def get_device_template( device_template_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralDeviceTemplateProviderPreview( + cmd=cmd, app_id=app_id, token=token + ) + else: + provider = CentralDeviceTemplateProviderV1(cmd=cmd, app_id=app_id, token=token) + template = provider.get_device_template( - device_template_id=device_template_id, central_dns_suffix=central_dns_suffix + device_template_id=device_template_id, central_dns_suffix=central_dns_suffix, ) return template.raw_template def list_device_templates( - cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT + cmd, + app_id: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralDeviceTemplateProviderPreview( + cmd=cmd, app_id=app_id, token=token + ) + else: + provider = CentralDeviceTemplateProviderV1(cmd=cmd, app_id=app_id, token=token) + templates = provider.list_device_templates(central_dns_suffix=central_dns_suffix) return {template.id: template.raw_template for template in templates.values()} def map_device_templates( - cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT + cmd, + app_id: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralDeviceTemplateProviderPreview( + cmd=cmd, app_id=app_id, token=token + ) + else: + provider = CentralDeviceTemplateProviderV1(cmd=cmd, app_id=app_id, token=token) + return provider.map_device_templates(central_dns_suffix=central_dns_suffix) @@ -48,13 +77,20 @@ def create_device_template( content: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): if not isinstance(content, str): raise CLIError("content must be a string: {}".format(content)) payload = utility.process_json_arg(content, argument_name="content") - provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralDeviceTemplateProviderPreview( + cmd=cmd, app_id=app_id, token=token + ) + else: + provider = CentralDeviceTemplateProviderV1(cmd=cmd, app_id=app_id, token=token) + template = provider.create_device_template( device_template_id=device_template_id, payload=payload, @@ -69,8 +105,15 @@ def delete_device_template( device_template_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralDeviceTemplateProviderPreview( + cmd=cmd, app_id=app_id, token=token + ) + else: + provider = CentralDeviceTemplateProviderV1(cmd=cmd, app_id=app_id, token=token) + return provider.delete_device_template( - device_template_id=device_template_id, central_dns_suffix=central_dns_suffix + device_template_id=device_template_id, central_dns_suffix=central_dns_suffix, ) diff --git a/azext_iot/central/commands_user.py b/azext_iot/central/commands_user.py index 63841f35a..5f79db8ac 100644 --- a/azext_iot/central/commands_user.py +++ b/azext_iot/central/commands_user.py @@ -7,8 +7,9 @@ from azext_iot.constants import CENTRAL_ENDPOINT -from azext_iot.central.providers import CentralUserProvider -from azext_iot.central.models.enum import Role +from azext_iot.central.providers.preview import CentralUserProviderPreview +from azext_iot.central.providers.v1 import CentralUserProviderV1 +from azext_iot.central.models.enum import Role, ApiVersion def add_user( @@ -21,8 +22,12 @@ def add_user( object_id=None, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralUserProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralUserProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralUserProviderV1(cmd=cmd, app_id=app_id, token=token) if email: return provider.add_email( @@ -42,26 +47,49 @@ def add_user( def list_users( - cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralUserProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralUserProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralUserProviderV1(cmd=cmd, app_id=app_id, token=token) return provider.get_user_list(central_dns_suffix=central_dns_suffix,) def get_user( - cmd, app_id: str, assignee: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + assignee: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralUserProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralUserProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralUserProviderV1(cmd=cmd, app_id=app_id, token=token) - return provider.get_user(assignee=assignee, central_dns_suffix=central_dns_suffix) + return provider.get_user(assignee=assignee, central_dns_suffix=central_dns_suffix,) def delete_user( - cmd, app_id: str, assignee: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + assignee: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): - provider = CentralUserProvider(cmd=cmd, app_id=app_id, token=token) + if api_version == ApiVersion.preview.value: + provider = CentralUserProviderPreview(cmd=cmd, app_id=app_id, token=token) + else: + provider = CentralUserProviderV1(cmd=cmd, app_id=app_id, token=token) return provider.delete_user( - assignee=assignee, central_dns_suffix=central_dns_suffix + assignee=assignee, central_dns_suffix=central_dns_suffix, ) diff --git a/azext_iot/central/models/__init__.py b/azext_iot/central/models/__init__.py index 55614acbf..cc1abd64c 100644 --- a/azext_iot/central/models/__init__.py +++ b/azext_iot/central/models/__init__.py @@ -3,3 +3,17 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azext_iot.central.models.devicePreview import DevicePreview +from azext_iot.central.models.devicev1 import DeviceV1 +from azext_iot.central.models.devicetwin import DeviceTwin +from azext_iot.central.models.templatepreview import TemplatePreview +from azext_iot.central.models.templatev1 import TemplateV1 + + +__all__ = [ + "DevicePreview", + "DeviceV1", + "DeviceTwin", + "TemplatePreview", + "TemplateV1", +] diff --git a/azext_iot/central/models/device.py b/azext_iot/central/models/devicePreview.py similarity index 93% rename from azext_iot/central/models/device.py rename to azext_iot/central/models/devicePreview.py index 7bc02bd78..b3f033903 100644 --- a/azext_iot/central/models/device.py +++ b/azext_iot/central/models/devicePreview.py @@ -7,10 +7,9 @@ from azext_iot.central.models.enum import DeviceStatus -class Device: +class DevicePreview: def __init__(self, device: dict): self.approved = device.get("approved") - self.description = device.get("description") self.display_name = device.get("displayName") self.etag = device.get("etag") self.id = device.get("id") @@ -32,7 +31,7 @@ def _parse_device_status(self) -> DeviceStatus: return DeviceStatus.provisioned - def get_registration_info(self): + def get_registration_info(self) -> dict: registration_info = { "device_status": self.device_status.value, "display_name": self.display_name, diff --git a/azext_iot/central/models/devicev1.py b/azext_iot/central/models/devicev1.py new file mode 100644 index 000000000..28f6bf4c1 --- /dev/null +++ b/azext_iot/central/models/devicev1.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. +# -------------------------------------------------------------------------------------------- + +from azext_iot.central.models.enum import DeviceStatus + + +class DeviceV1: + def __init__(self, device: dict): + self.enabled = device.get("enabled") + self.display_name = device.get("displayName") + self.etag = device.get("etag") + self.id = device.get("id") + self.template = device.get("template") + self.provisioned = device.get("provisioned") + self.simulated = device.get("simulated") + self.device_status = self._parse_device_status() + + def _parse_device_status(self) -> DeviceStatus: + if not self.enabled: + return DeviceStatus.blocked + + if not self.template: + return DeviceStatus.unassociated + + if not self.provisioned: + return DeviceStatus.registered + + return DeviceStatus.provisioned + + def get_registration_info(self) -> dict: + registration_info = { + "device_status": self.device_status.value, + "display_name": self.display_name, + "id": self.id, + "simulated": self.simulated, + "template": self.template, + } + + return registration_info diff --git a/azext_iot/central/models/enum.py b/azext_iot/central/models/enum.py index 1e4e976c5..16eea4ac2 100644 --- a/azext_iot/central/models/enum.py +++ b/azext_iot/central/models/enum.py @@ -33,11 +33,30 @@ class Role(Enum): operator = "ae2c9854-393b-4f97-8c42-479d70ce626e" -class UserType(Enum): +class UserTypePreview(Enum): """ - Types of users that can be added to use/manage a Central app + Types of users , supported under the preview route, that can be added to use/manage a Central app (service principal, email, etc) """ service_principal = "ServicePrincipalUser" email = "EmailUser" + + +class UserTypeV1(Enum): + """ + Types of users, supported under V1/1.0 route, that can be added to use/manage a Central app + (service principal, email, etc) + """ + + service_principal = "servicePrincipal" + email = "email" + + +class ApiVersion(Enum): + """ + API version's supported + """ + + preview = "preview" + v1 = "1.0" diff --git a/azext_iot/central/models/template.py b/azext_iot/central/models/templatepreview.py similarity index 93% rename from azext_iot/central/models/template.py rename to azext_iot/central/models/templatepreview.py index cfb767a84..48f6e23d1 100644 --- a/azext_iot/central/models/template.py +++ b/azext_iot/central/models/templatepreview.py @@ -7,7 +7,7 @@ from knack.util import CLIError -class Template: +class TemplatePreview: def __init__(self, template: dict): self.raw_template = template try: @@ -81,10 +81,8 @@ def _extract_interfaces(self, template: dict) -> dict: if dcm.get("contents"): interfaces.append(self._extract_root_interface_contents(dcm)) - if dcm.get("@type") == "CapabilityModel": + if dcm.get("implements"): interfaces.extend(dcm.get("implements")) - else: - interfaces.extend(dcm.get("extends")) return { interface["@id"]: self._extract_schemas(interface) @@ -97,7 +95,8 @@ def _extract_interfaces(self, template: dict) -> dict: raise CLIError(details) def _extract_schemas(self, entity: dict) -> dict: - return {schema["name"]: schema for schema in entity["schema"]["contents"]} + if entity.get("schema"): + return {schema["name"]: schema for schema in entity["schema"]["contents"]} def _extract_schema_names(self, entity: dict) -> dict: return { @@ -105,7 +104,7 @@ def _extract_schema_names(self, entity: dict) -> dict: for entity_name, entity_schemas in entity.items() } - def _get_interface_list_property(self, property_name): + def _get_interface_list_property(self, property_name) -> list: # returns the list of interfaces where property with property_name is defined return [ interface diff --git a/azext_iot/central/models/templatev1.py b/azext_iot/central/models/templatev1.py new file mode 100644 index 000000000..20d439a3b --- /dev/null +++ b/azext_iot/central/models/templatev1.py @@ -0,0 +1,115 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from knack.util import CLIError + + +class TemplateV1: + def __init__(self, template: dict): + self.raw_template = template + try: + self.id = template.get("id") + self.name = template.get("displayName") + self.interfaces = self._extract_interfaces(template) + self.schema_names = self._extract_schema_names(self.interfaces) + self.components = self._extract_components(template) + if self.components: + self.component_schema_names = self._extract_schema_names( + self.components + ) + + except: + raise CLIError("Could not parse iot central device template.") + + def get_schema(self, name, is_component=False, identifier="") -> dict: + entities = self.components if is_component else self.interfaces + if identifier: + # identifier specified, do a pointed lookup + entry = entities.get(identifier, {}) + return entry.get(name) + + # find first matching name in any component + for entry in entities.values(): + schema = entry.get(name) + if schema: + return schema + + # not found + return None + + def _extract_components(self, template: dict) -> dict: + try: + dcm = template.get("capabilityModel", {}) + if dcm.get("contents"): + rootContents = dcm.get("contents", {}) + components = [ + entity + for entity in rootContents + if entity.get("@type") == "Component" + ] + + if components: + return { + component["name"]: self._extract_schemas(component) + for component in components + } + return {} + return {} + except Exception: + details = "Unable to extract schema for component from template '{}'.".format( + self.id + ) + raise CLIError(details) + + def _extract_root_interface_contents(self, dcm: dict) -> dict: + rootContents = dcm.get("contents", {}) + contents = [ + entity for entity in rootContents if entity.get("@type") != "Component" + ] + + return {"@id": dcm.get("@id", {}), "schema": {"contents": contents}} + + def _extract_interfaces(self, template: dict) -> dict: + try: + interfaces = [] + dcm = template.get("capabilityModel", {}) + + if dcm.get("contents"): + interfaces.append(self._extract_root_interface_contents(dcm)) + + if dcm.get("extends"): + interfaces.extend(dcm.get("extends")) + + return { + interface["@id"]: self._extract_schemas(interface) + for interface in interfaces + } + except Exception: + details = "Unable to extract device schema from template '{}'.".format( + self.id + ) + raise CLIError(details) + + def _extract_schemas(self, entity: dict) -> dict: + if entity.get("schema"): + return {schema["name"]: schema for schema in entity["schema"]["contents"]} + else: + return {schema["name"]: schema for schema in entity["contents"]} + + def _extract_schema_names(self, entity: dict) -> dict: + return { + entity_name: list(entity_schemas.keys()) + for entity_name, entity_schemas in entity.items() + } + + def _get_interface_list_property(self, property_name) -> list: + # returns the list of interfaces where property with property_name is defined + return [ + interface + for interface, schema in self.schema_names.items() + if property_name in schema + ] diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index b543ecc53..fe1a2e4c6 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -11,7 +11,7 @@ from knack.arguments import CLIArgumentType, CaseInsensitiveList from azure.cli.core.commands.parameters import get_three_state_flag from azext_iot.monitor.models.enum import Severity -from azext_iot.central.models.enum import Role +from azext_iot.central.models.enum import Role, ApiVersion from azext_iot._params import event_msg_prop_type, event_timeout_type severity_type = CLIArgumentType( @@ -35,6 +35,13 @@ "scroll = deliver errors as they arrive, json = summarize results as json, csv = summarize results as csv", ) +api_version = CLIArgumentType( + options_list=["--api-version", "--av"], + choices=CaseInsensitiveList([version.value for version in ApiVersion]), + default=ApiVersion.v1.value, + help="The API version for the requested operation.", +) + def load_central_arguments(self, _): """ @@ -45,8 +52,9 @@ def load_central_arguments(self, _): "app_id", options_list=["--app-id", "-n"], help="The App ID of the IoT Central app you want to manage." - " You can find the App ID in the \"About\" page for your application under the help menu." + ' You can find the App ID in the "About" page for your application under the help menu.', ) + context.argument("api_version", arg_type=api_version) context.argument( "token", options_list=["--token"], @@ -105,16 +113,16 @@ def load_central_arguments(self, _): with self.argument_context("iot central device") as context: context.argument( - "instance_of", - options_list=["--instance-of"], - help="Central template id. Example: urn:ojpkindbz:modelDefinition:iild3tm_uo", + "template", + options_list=["--template"], + help="Central template id. Example: dtmi:ojpkindbz:modelDefinition:iild3tm_uo.", ) context.argument( "simulated", options_list=["--simulated"], arg_type=get_three_state_flag(), help="Add this flag if you would like IoT Central to set this up as a simulated device. " - "--instance-of is required if this is true", + "--template is required if this is true", ) context.argument( "device_name", @@ -203,5 +211,7 @@ def load_central_arguments(self, _): "Use 0 for infinity.", ) context.argument( - "module_id", options_list=["--module-id", "-m"], help="Provide IoT Edge Module ID if the device type is IoT Edge.", + "module_id", + options_list=["--module-id", "-m"], + help="Provide IoT Edge Module ID if the device type is IoT Edge.", ) diff --git a/azext_iot/central/providers/__init__.py b/azext_iot/central/providers/__init__.py index 451120fe6..64ee059b6 100644 --- a/azext_iot/central/providers/__init__.py +++ b/azext_iot/central/providers/__init__.py @@ -4,18 +4,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_iot.central.providers.device_provider import CentralDeviceProvider -from azext_iot.central.providers.device_template_provider import ( - CentralDeviceTemplateProvider, -) from azext_iot.central.providers.devicetwin_provider import CentralDeviceTwinProvider -from azext_iot.central.providers.user_provider import CentralUserProvider -from azext_iot.central.providers.api_token_provider import CentralApiTokenProvider __all__ = [ - "CentralDeviceProvider", - "CentralDeviceTemplateProvider", "CentralDeviceTwinProvider", - "CentralUserProvider", - "CentralApiTokenProvider", ] diff --git a/azext_iot/central/providers/devicetwin_provider.py b/azext_iot/central/providers/devicetwin_provider.py index 836676742..4a4542620 100644 --- a/azext_iot/central/providers/devicetwin_provider.py +++ b/azext_iot/central/providers/devicetwin_provider.py @@ -50,9 +50,13 @@ def get_device_twin(self, central_dns_suffix): endpoint = find_between(sas_token, "SharedAccessSignature sr=", "&sig=") target = {"entity": endpoint} auth = BasicSasTokenAuthentication(sas_token=sas_token) - service_sdk = SdkResolver(target=target, auth_override=auth).get_sdk(SdkType.service_sdk) + service_sdk = SdkResolver(target=target, auth_override=auth).get_sdk( + SdkType.service_sdk + ) try: - return service_sdk.devices.get_twin(id=self._device_id, raw=True).response.json() + return service_sdk.devices.get_twin( + id=self._device_id, raw=True + ).response.json() except CloudError as e: if exception is None: exception = CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/central/providers/monitor_provider.py b/azext_iot/central/providers/monitor_provider.py index 555db570c..76b877e27 100644 --- a/azext_iot/central/providers/monitor_provider.py +++ b/azext_iot/central/providers/monitor_provider.py @@ -6,9 +6,9 @@ from azure.cli.core.commands import AzCliCommand -from azext_iot.central.providers import ( - CentralDeviceProvider, - CentralDeviceTemplateProvider, +from azext_iot.central.providers.v1 import ( + CentralDeviceProviderV1, + CentralDeviceTemplateProviderV1, ) from azext_iot.monitor.models.arguments import ( @@ -31,10 +31,10 @@ def __init__( central_handler_args: CentralHandlerArguments, central_dns_suffix: str, ): - central_device_provider = CentralDeviceProvider( + central_device_provider = CentralDeviceProviderV1( cmd=cmd, app_id=app_id, token=token ) - central_template_provider = CentralDeviceTemplateProvider( + central_template_provider = CentralDeviceTemplateProviderV1( cmd=cmd, app_id=app_id, token=token ) self._targets = self._build_targets( @@ -91,8 +91,8 @@ def _build_targets( def _build_handler( self, - central_device_provider: CentralDeviceProvider, - central_template_provider: CentralDeviceTemplateProvider, + central_device_provider: CentralDeviceProviderV1, + central_template_provider: CentralDeviceTemplateProviderV1, central_handler_args: CentralHandlerArguments, ): from azext_iot.monitor.handlers import CentralHandler diff --git a/azext_iot/central/providers/preview/__init__.py b/azext_iot/central/providers/preview/__init__.py new file mode 100644 index 000000000..f47cdd416 --- /dev/null +++ b/azext_iot/central/providers/preview/__init__.py @@ -0,0 +1,26 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.central.providers.preview.device_provider_preview import ( + CentralDeviceProviderPreview, +) +from azext_iot.central.providers.preview.device_template_provider_preview import ( + CentralDeviceTemplateProviderPreview, +) + +from azext_iot.central.providers.preview.user_provider_preview import ( + CentralUserProviderPreview, +) +from azext_iot.central.providers.preview.api_token_provider_preview import ( + CentralApiTokenProviderPreview, +) + +__all__ = [ + "CentralDeviceProviderPreview", + "CentralDeviceTemplateProviderPreview", + "CentralUserProviderPreview", + "CentralApiTokenProviderPreview", +] diff --git a/azext_iot/central/providers/preview/api_token_provider_preview.py b/azext_iot/central/providers/preview/api_token_provider_preview.py new file mode 100644 index 000000000..60601c178 --- /dev/null +++ b/azext_iot/central/providers/preview/api_token_provider_preview.py @@ -0,0 +1,81 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.log import get_logger + +from azext_iot.constants import CENTRAL_ENDPOINT +from azext_iot.central import services as central_services +from azext_iot.central.models.enum import Role, ApiVersion + + +logger = get_logger(__name__) + + +class CentralApiTokenProviderPreview: + def __init__(self, cmd, app_id: str, token=None): + """ + Provider for API token APIs + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch API token details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + Useful in scenarios where user doesn't own the app + therefore AAD token won't work, but a SAS token generated by owner will + """ + self._cmd = cmd + self._app_id = app_id + self._token = token + + def add_api_token( + self, token_id: str, role: Role, central_dns_suffix=CENTRAL_ENDPOINT, + ) -> dict: + + return central_services.api_token.add_api_token( + cmd=self._cmd, + app_id=self._app_id, + token_id=token_id, + role=role, + token=self._token, + api_version=ApiVersion.preview.value, + central_dns_suffix=central_dns_suffix, + ) + + def get_api_token_list(self, central_dns_suffix=CENTRAL_ENDPOINT) -> dict: + + return central_services.api_token.get_api_token_list( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + api_version=ApiVersion.preview.value, + central_dns_suffix=central_dns_suffix, + ) + + def get_api_token(self, token_id, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: + return central_services.api_token.get_api_token( + cmd=self._cmd, + app_id=self._app_id, + token_id=token_id, + token=self._token, + api_version=ApiVersion.preview.value, + central_dns_suffix=central_dns_suffix, + ) + + def delete_api_token( + self, + token_id, + api_version=ApiVersion.v1.value, + central_dns_suffix=CENTRAL_ENDPOINT, + ) -> dict: + return central_services.api_token.delete_api_token( + cmd=self._cmd, + app_id=self._app_id, + token_id=token_id, + token=self._token, + api_version=ApiVersion.preview.value, + central_dns_suffix=central_dns_suffix, + ) diff --git a/azext_iot/central/providers/preview/device_provider_preview.py b/azext_iot/central/providers/preview/device_provider_preview.py new file mode 100644 index 000000000..49368258e --- /dev/null +++ b/azext_iot/central/providers/preview/device_provider_preview.py @@ -0,0 +1,182 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from typing import List +from azext_iot.central.models.devicePreview import DevicePreview +from knack.util import CLIError +from knack.log import get_logger +from azext_iot.constants import CENTRAL_ENDPOINT +from azext_iot.central import services as central_services +from azext_iot.central.models.enum import DeviceStatus, ApiVersion +from azext_iot.central import models as central_models + +logger = get_logger(__name__) + + +class CentralDeviceProviderPreview: + def __init__(self, cmd, app_id: str, token=None): + """ + Provider for device APIs + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + Useful in scenarios where user doesn't own the app + therefore AAD token won't work, but a SAS token generated by owner will + """ + self._cmd = cmd + self._app_id = app_id + self._token = token + self._devices = {} + self._device_templates = {} + self._device_credentials = {} + self._device_registration_info = {} + + def get_device( + self, device_id, central_dns_suffix=CENTRAL_ENDPOINT, + ) -> central_models.DevicePreview: + # get or add to cache + device = self._devices.get(device_id) + if not device: + device = central_services.device.get_device( + cmd=self._cmd, + app_id=self._app_id, + device_id=device_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + self._devices[device_id] = device + + if not device: + raise CLIError("No device found with id: '{}'.".format(device_id)) + + return device + + def list_devices(self, central_dns_suffix=CENTRAL_ENDPOINT) -> List[DevicePreview]: + devices = central_services.device.list_devices( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + # add to cache + self._devices.update({device.id: device for device in devices}) + + return self._devices + + def create_device( + self, + device_id, + device_name=None, + template=None, + simulated=False, + central_dns_suffix=CENTRAL_ENDPOINT, + ) -> central_models.DevicePreview: + + if device_id in self._devices: + raise CLIError("Device already exists.") + + device = central_services.device.create_device( + cmd=self._cmd, + app_id=self._app_id, + device_id=device_id, + device_name=device_name, + template=template, + simulated=simulated, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + if not device: + raise CLIError("No device found with id: '{}'.".format(device_id)) + + # add to cache + self._devices[device.id] = device + + return device + + def delete_device(self, device_id, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: + if not device_id: + raise CLIError("Device id must be specified.") + + # get or add to cache + result = central_services.device.delete_device( + cmd=self._cmd, + app_id=self._app_id, + device_id=device_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + # remove from cache + # pop "miss" raises a KeyError if None is not provided + self._devices.pop(device_id, None) + self._device_credentials.pop(device_id, None) + + return result + + def run_component_command( + self, + device_id: str, + interface_id: str, + command_name: str, + payload: dict, + central_dns_suffix=CENTRAL_ENDPOINT, + ) -> dict: + return central_services.device.run_component_command( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + device_id=device_id, + interface_id=interface_id, + command_name=command_name, + payload=payload, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + def get_component_command_history( + self, + device_id: str, + interface_id: str, + command_name: str, + central_dns_suffix=CENTRAL_ENDPOINT, + ) -> dict: + return central_services.device.get_component_command_history( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + device_id=device_id, + interface_id=interface_id, + command_name=command_name, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + def _dps_populate_essential_info( + self, dps_info, device_status: DeviceStatus + ) -> dict: + error = { + DeviceStatus.provisioned: "None.", + DeviceStatus.registered: "Device is not yet provisioned.", + DeviceStatus.blocked: "Device is blocked from connecting to IoT Central application." + " Unblock the device in IoT Central and retry. Learn more: https://aka.ms/iotcentral-docs-dps-SAS", + DeviceStatus.unassociated: "Device does not have a valid template associated with it.", + } + + filtered_dps_info = { + "status": dps_info.get("status"), + "error": error.get(device_status), + } + return filtered_dps_info diff --git a/azext_iot/central/providers/preview/device_template_provider_preview.py b/azext_iot/central/providers/preview/device_template_provider_preview.py new file mode 100644 index 000000000..63ad41406 --- /dev/null +++ b/azext_iot/central/providers/preview/device_template_provider_preview.py @@ -0,0 +1,124 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from typing import List +from knack.util import CLIError +from azext_iot.constants import CENTRAL_ENDPOINT +from azext_iot.central import services as central_services +from azext_iot.central.models.enum import ApiVersion +from azext_iot.central import models as central_models + + +class CentralDeviceTemplateProviderPreview: + def __init__(self, cmd, app_id, token=None): + """ + Provider for device_template APIs + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + Useful in scenarios where user doesn't own the app + therefore AAD token won't work, but a SAS token generated by owner will + """ + self._cmd = cmd + self._app_id = app_id + self._token = token + self._device_templates = {} + + def get_device_template( + self, device_template_id, central_dns_suffix=CENTRAL_ENDPOINT, + ) -> central_models.TemplatePreview: + # get or add to cache + device_template = self._device_templates.get(device_template_id) + if not device_template: + device_template = central_services.device_template.get_device_template( + cmd=self._cmd, + app_id=self._app_id, + device_template_id=device_template_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + self._device_templates[device_template_id] = device_template + + if not device_template: + raise CLIError( + "No device template for device template with id: '{}'.".format( + device_template_id + ) + ) + + return device_template + + def list_device_templates( + self, central_dns_suffix=CENTRAL_ENDPOINT, + ) -> List[central_models.TemplatePreview]: + templates = central_services.device_template.list_device_templates( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + self._device_templates.update({template.id: template for template in templates}) + + return self._device_templates + + def map_device_templates(self, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: + """ + Maps each template name to the corresponding template id + """ + templates = central_services.device_template.list_device_templates( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + api_version=ApiVersion.preview.value, + ) + return {template.name: template.id for template in templates} + + def create_device_template( + self, + device_template_id: str, + payload: str, + central_dns_suffix=CENTRAL_ENDPOINT, + ) -> central_models.TemplatePreview: + template = central_services.device_template.create_device_template( + cmd=self._cmd, + app_id=self._app_id, + device_template_id=device_template_id, + payload=payload, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + self._device_templates[template.id] = template + + return template + + def delete_device_template( + self, device_template_id, central_dns_suffix=CENTRAL_ENDPOINT, + ) -> dict: + if not device_template_id: + raise CLIError("Device template id must be specified.") + + result = central_services.device_template.delete_device_template( + cmd=self._cmd, + token=self._token, + app_id=self._app_id, + device_template_id=device_template_id, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + # remove from cache + # pop "miss" raises a KeyError if None is not provided + self._device_templates.pop(device_template_id, None) + + return result diff --git a/azext_iot/central/providers/preview/user_provider_preview.py b/azext_iot/central/providers/preview/user_provider_preview.py new file mode 100644 index 000000000..b1aa5517f --- /dev/null +++ b/azext_iot/central/providers/preview/user_provider_preview.py @@ -0,0 +1,109 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.log import get_logger +from knack.util import CLIError + +from azext_iot.constants import CENTRAL_ENDPOINT +from azext_iot.central import services as central_services +from azext_iot.central.models.enum import Role, ApiVersion + + +logger = get_logger(__name__) + + +class CentralUserProviderPreview: + def __init__(self, cmd, app_id: str, token=None): + """ + Provider for device APIs + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + Useful in scenarios where user doesn't own the app + therefore AAD token won't work, but a SAS token generated by owner will + """ + self._cmd = cmd + self._app_id = app_id + self._token = token + + def add_service_principal( + self, + assignee: str, + tenant_id: str, + object_id: str, + role: Role, + central_dns_suffix=CENTRAL_ENDPOINT, + ) -> dict: + if not tenant_id: + raise CLIError("Must specify --tenant-id when adding a service principal") + + if not object_id: + raise CLIError("Must specify --object-id when adding a service principal") + + return central_services.user.add_service_principal( + cmd=self._cmd, + app_id=self._app_id, + assignee=assignee, + tenant_id=tenant_id, + object_id=object_id, + role=role, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + def get_user_list(self, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: + return central_services.user.get_user_list( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + def get_user(self, assignee, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: + return central_services.user.get_user( + cmd=self._cmd, + app_id=self._app_id, + assignee=assignee, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + def delete_user(self, assignee, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: + return central_services.user.delete_user( + cmd=self._cmd, + app_id=self._app_id, + assignee=assignee, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) + + def add_email( + self, + assignee: str, + email: str, + role: Role, + central_dns_suffix=CENTRAL_ENDPOINT, + ): + if not email: + raise CLIError("Must specify --email when adding a user by email") + + return central_services.user.add_email( + cmd=self._cmd, + app_id=self._app_id, + assignee=assignee, + email=email, + role=role, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.preview.value, + ) diff --git a/azext_iot/central/providers/v1/__init__.py b/azext_iot/central/providers/v1/__init__.py new file mode 100644 index 000000000..1befc5ee3 --- /dev/null +++ b/azext_iot/central/providers/v1/__init__.py @@ -0,0 +1,22 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.central.providers.v1.device_provider_v1 import CentralDeviceProviderV1 +from azext_iot.central.providers.v1.device_template_provider_v1 import ( + CentralDeviceTemplateProviderV1, +) + +from azext_iot.central.providers.v1.user_provider_v1 import CentralUserProviderV1 +from azext_iot.central.providers.v1.api_token_provider_v1 import ( + CentralApiTokenProviderV1, +) + +__all__ = [ + "CentralDeviceProviderV1", + "CentralDeviceTemplateProviderV1", + "CentralUserProviderV1", + "CentralApiTokenProviderV1", +] diff --git a/azext_iot/central/providers/api_token_provider.py b/azext_iot/central/providers/v1/api_token_provider_v1.py similarity index 80% rename from azext_iot/central/providers/api_token_provider.py rename to azext_iot/central/providers/v1/api_token_provider_v1.py index b76574513..51b03b176 100644 --- a/azext_iot/central/providers/api_token_provider.py +++ b/azext_iot/central/providers/v1/api_token_provider_v1.py @@ -8,13 +8,13 @@ from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central import services as central_services -from azext_iot.central.models.enum import Role +from azext_iot.central.models.enum import Role, ApiVersion logger = get_logger(__name__) -class CentralApiTokenProvider: +class CentralApiTokenProviderV1: def __init__(self, cmd, app_id: str, token=None): """ Provider for API token APIs @@ -33,7 +33,7 @@ def __init__(self, cmd, app_id: str, token=None): def add_api_token( self, token_id: str, role: Role, central_dns_suffix=CENTRAL_ENDPOINT, - ): + ) -> dict: return central_services.api_token.add_api_token( cmd=self._cmd, @@ -41,38 +41,36 @@ def add_api_token( token_id=token_id, role=role, token=self._token, + api_version=ApiVersion.v1.value, central_dns_suffix=central_dns_suffix, ) - def get_api_token_list( - self, central_dns_suffix=CENTRAL_ENDPOINT, - ): + def get_api_token_list(self, central_dns_suffix=CENTRAL_ENDPOINT) -> dict: return central_services.api_token.get_api_token_list( cmd=self._cmd, app_id=self._app_id, token=self._token, + api_version=ApiVersion.v1.value, central_dns_suffix=central_dns_suffix, ) - def get_api_token( - self, token_id, central_dns_suffix=CENTRAL_ENDPOINT, - ): + def get_api_token(self, token_id, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: return central_services.api_token.get_api_token( cmd=self._cmd, app_id=self._app_id, token_id=token_id, token=self._token, + api_version=ApiVersion.v1.value, central_dns_suffix=central_dns_suffix, ) - def delete_api_token( - self, token_id, central_dns_suffix=CENTRAL_ENDPOINT, - ): + def delete_api_token(self, token_id, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: return central_services.api_token.delete_api_token( cmd=self._cmd, app_id=self._app_id, token_id=token_id, token=self._token, + api_version=ApiVersion.v1.value, central_dns_suffix=central_dns_suffix, ) diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/v1/device_provider_v1.py similarity index 92% rename from azext_iot/central/providers/device_provider.py rename to azext_iot/central/providers/v1/device_provider_v1.py index 43cb5ed12..ceed21644 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/v1/device_provider_v1.py @@ -4,20 +4,20 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from typing import List +from azext_iot.central.models.devicev1 import DeviceV1 from knack.util import CLIError from knack.log import get_logger -from typing import List from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central import services as central_services -from azext_iot.central.models.enum import DeviceStatus -from azext_iot.central.models.device import Device +from azext_iot.central.models.enum import DeviceStatus, ApiVersion from azext_iot.dps.services import global_service as dps_global_service logger = get_logger(__name__) -class CentralDeviceProvider: +class CentralDeviceProviderV1: def __init__(self, cmd, app_id: str, token=None): """ Provider for device APIs @@ -38,9 +38,8 @@ def __init__(self, cmd, app_id: str, token=None): self._device_credentials = {} self._device_registration_info = {} - def get_device(self, device_id, central_dns_suffix=CENTRAL_ENDPOINT) -> Device: - if not device_id: - raise CLIError("Device id must be specified.") + def get_device(self, device_id, central_dns_suffix=CENTRAL_ENDPOINT,) -> DeviceV1: + # get or add to cache device = self._devices.get(device_id) if not device: @@ -50,6 +49,7 @@ def get_device(self, device_id, central_dns_suffix=CENTRAL_ENDPOINT) -> Device: device_id=device_id, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) self._devices[device_id] = device @@ -58,12 +58,13 @@ def get_device(self, device_id, central_dns_suffix=CENTRAL_ENDPOINT) -> Device: return device - def list_devices(self, central_dns_suffix=CENTRAL_ENDPOINT) -> List[Device]: + def list_devices(self, central_dns_suffix=CENTRAL_ENDPOINT,) -> List[DeviceV1]: devices = central_services.device.list_devices( cmd=self._cmd, app_id=self._app_id, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) # add to cache @@ -75,10 +76,10 @@ def create_device( self, device_id, device_name=None, - instance_of=None, + template=None, simulated=False, central_dns_suffix=CENTRAL_ENDPOINT, - ) -> Device: + ) -> DeviceV1: if not device_id: raise CLIError("Device id must be specified.") @@ -90,10 +91,11 @@ def create_device( app_id=self._app_id, device_id=device_id, device_name=device_name, - instance_of=instance_of, + template=template, simulated=simulated, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) if not device: @@ -115,6 +117,7 @@ def delete_device(self, device_id, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict device_id=device_id, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) # remove from cache @@ -136,6 +139,7 @@ def get_device_credentials( device_id=device_id, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) if not credentials: @@ -163,7 +167,7 @@ def get_device_registration_info( device = self.get_device(device_id, central_dns_suffix) if device.device_status == DeviceStatus.provisioned: credentials = self.get_device_credentials( - device_id=device_id, central_dns_suffix=central_dns_suffix + device_id=device_id, central_dns_suffix=central_dns_suffix, ) id_scope = credentials["idScope"] key = credentials["symmetricKey"]["primaryKey"] @@ -207,6 +211,7 @@ def run_component_command( command_name=command_name, payload=payload, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) def get_component_command_history( @@ -224,6 +229,7 @@ def get_component_command_history( interface_id=interface_id, command_name=command_name, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) def run_manual_failover( @@ -242,9 +248,7 @@ def run_manual_failover( ) def run_manual_failback( - self, - device_id: str, - central_dns_suffix=CENTRAL_ENDPOINT, + self, device_id: str, central_dns_suffix=CENTRAL_ENDPOINT, ): return central_services.device.run_manual_failback( cmd=self._cmd, diff --git a/azext_iot/central/providers/device_template_provider.py b/azext_iot/central/providers/v1/device_template_provider_v1.py similarity index 82% rename from azext_iot/central/providers/device_template_provider.py rename to azext_iot/central/providers/v1/device_template_provider_v1.py index 2a8f72597..5495883bc 100644 --- a/azext_iot/central/providers/device_template_provider.py +++ b/azext_iot/central/providers/v1/device_template_provider_v1.py @@ -4,12 +4,15 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from typing import List from knack.util import CLIError from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central import services as central_services +from azext_iot.central.models.enum import ApiVersion +from azext_iot.central import models as central_models -class CentralDeviceTemplateProvider: +class CentralDeviceTemplateProviderV1: def __init__(self, cmd, app_id, token=None): """ Provider for device_template APIs @@ -29,7 +32,7 @@ def __init__(self, cmd, app_id, token=None): def get_device_template( self, device_template_id, central_dns_suffix=CENTRAL_ENDPOINT, - ): + ) -> central_models.TemplateV1: # get or add to cache device_template = self._device_templates.get(device_template_id) if not device_template: @@ -39,6 +42,7 @@ def get_device_template( device_template_id=device_template_id, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) self._device_templates[device_template_id] = device_template @@ -51,11 +55,13 @@ def get_device_template( return device_template - def list_device_templates( - self, central_dns_suffix=CENTRAL_ENDPOINT, - ): + def list_device_templates(self, central_dns_suffix=CENTRAL_ENDPOINT,) -> List[central_models.TemplateV1] : templates = central_services.device_template.list_device_templates( - cmd=self._cmd, app_id=self._app_id, token=self._token + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) self._device_templates.update({template.id: template for template in templates}) @@ -69,7 +75,10 @@ def map_device_templates( Maps each template name to the corresponding template id """ templates = central_services.device_template.list_device_templates( - cmd=self._cmd, app_id=self._app_id, token=self._token + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + api_version=ApiVersion.v1.value, ) return {template.name: template.id for template in templates} @@ -86,6 +95,7 @@ def create_device_template( payload=payload, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) self._device_templates[template.id] = template @@ -104,6 +114,7 @@ def delete_device_template( app_id=self._app_id, device_template_id=device_template_id, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) # remove from cache diff --git a/azext_iot/central/providers/user_provider.py b/azext_iot/central/providers/v1/user_provider_v1.py similarity index 84% rename from azext_iot/central/providers/user_provider.py rename to azext_iot/central/providers/v1/user_provider_v1.py index f69ef1903..2f50153a7 100644 --- a/azext_iot/central/providers/user_provider.py +++ b/azext_iot/central/providers/v1/user_provider_v1.py @@ -9,13 +9,13 @@ from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central import services as central_services -from azext_iot.central.models.enum import Role +from azext_iot.central.models.enum import Role, ApiVersion logger = get_logger(__name__) -class CentralUserProvider: +class CentralUserProviderV1: def __init__(self, cmd, app_id: str, token=None): """ Provider for device APIs @@ -39,7 +39,7 @@ def add_service_principal( object_id: str, role: Role, central_dns_suffix=CENTRAL_ENDPOINT, - ): + ) -> dict: if not tenant_id: raise CLIError("Must specify --tenant-id when adding a service principal") @@ -55,38 +55,36 @@ def add_service_principal( role=role, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) - def get_user_list( - self, central_dns_suffix=CENTRAL_ENDPOINT, - ): + def get_user_list(self, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: return central_services.user.get_user_list( cmd=self._cmd, app_id=self._app_id, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) - def get_user( - self, assignee, central_dns_suffix=CENTRAL_ENDPOINT, - ): + def get_user(self, assignee, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: return central_services.user.get_user( cmd=self._cmd, app_id=self._app_id, assignee=assignee, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) - def delete_user( - self, assignee, central_dns_suffix=CENTRAL_ENDPOINT, - ): + def delete_user(self, assignee, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: return central_services.user.delete_user( cmd=self._cmd, app_id=self._app_id, assignee=assignee, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) def add_email( @@ -95,7 +93,7 @@ def add_email( email: str, role: Role, central_dns_suffix=CENTRAL_ENDPOINT, - ): + ) -> dict: if not email: raise CLIError("Must specify --email when adding a user by email") @@ -107,4 +105,5 @@ def add_email( role=role, token=self._token, central_dns_suffix=central_dns_suffix, + api_version=ApiVersion.v1.value, ) diff --git a/azext_iot/central/services/api_token.py b/azext_iot/central/services/api_token.py index fd1a0123d..2e2038637 100644 --- a/azext_iot/central/services/api_token.py +++ b/azext_iot/central/services/api_token.py @@ -9,11 +9,11 @@ from knack.log import get_logger from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central.services import _utility -from azext_iot.central.models.enum import Role +from azext_iot.central.models.enum import Role, ApiVersion logger = get_logger(__name__) -BASE_PATH = "api/preview/apiTokens" +BASE_PATH = "api/apiTokens" def add_api_token( @@ -22,8 +22,9 @@ def add_api_token( token_id: str, role: Role, token: str, + api_version=ApiVersion.v1.value, central_dns_suffix=CENTRAL_ENDPOINT, -): +) -> dict: """ Add an API token to a Central app @@ -45,15 +46,23 @@ def add_api_token( "roles": [{"role": role.value}], } + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + headers = _utility.get_headers(token, cmd, has_json_payload=True) - response = requests.put(url, headers=headers, json=payload) + response = requests.put(url, headers=headers, json=payload, params=query_parameters) return _utility.try_extract_result(response) def get_api_token_list( - cmd, app_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -): + cmd, + app_id: str, + token: str, + api_version: ApiVersion.v1.value, + central_dns_suffix=CENTRAL_ENDPOINT, +) -> dict: """ Get the list of API tokens for a central app. @@ -69,15 +78,24 @@ def get_api_token_list( """ url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers) + response = requests.get(url, params=query_parameters, headers=headers) return _utility.try_extract_result(response) def get_api_token( - cmd, app_id: str, token: str, token_id: str, central_dns_suffix=CENTRAL_ENDPOINT, -): + cmd, + app_id: str, + token: str, + token_id: str, + api_version=ApiVersion.v1.value, + central_dns_suffix=CENTRAL_ENDPOINT, +) -> dict: """ Get information about a specified API token. @@ -96,13 +114,22 @@ def get_api_token( headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.get(url, headers=headers, params=query_parameters) return _utility.try_extract_result(response) def delete_api_token( - cmd, app_id: str, token: str, token_id: str, central_dns_suffix=CENTRAL_ENDPOINT, -): + cmd, + app_id: str, + token: str, + token_id: str, + api_version=ApiVersion.v1.value, + central_dns_suffix=CENTRAL_ENDPOINT, +) -> dict: """ delete API token from the app. @@ -121,5 +148,9 @@ def delete_api_token( headers = _utility.get_headers(token, cmd) - response = requests.delete(url, headers=headers) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.delete(url, headers=headers, params=query_parameters) return _utility.try_extract_result(response) diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index 1d44c191b..21e2c78f9 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -5,26 +5,31 @@ # -------------------------------------------------------------------------------------------- # This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devices +from typing import List, Union import requests from knack.util import CLIError from knack.log import get_logger -from typing import List from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central.services import _utility -from azext_iot.central.models.device import Device -from azext_iot.central.models.enum import DeviceStatus +from azext_iot.central import models as central_models +from azext_iot.central.models.enum import DeviceStatus, ApiVersion from azure.cli.core.util import should_disable_connection_verify logger = get_logger(__name__) -BASE_PATH = "api/preview/devices" +BASE_PATH = "api/devices" def get_device( - cmd, app_id: str, device_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -) -> Device: + cmd, + app_id: str, + device_id: str, + token: str, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, +) -> Union[central_models.DevicePreview, central_models.DeviceV1]: """ Get device info given a device id @@ -43,14 +48,32 @@ def get_device( url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, device_id) headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers, verify=not should_disable_connection_verify()) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.get( + url, + headers=headers, + params=query_parameters, + verify=not should_disable_connection_verify(), + ) result = _utility.try_extract_result(response) - return Device(result) + + if api_version == ApiVersion.preview.value: + return central_models.DevicePreview(result) + else: + return central_models.DeviceV1(result) def list_devices( - cmd, app_id: str, token: str, max_pages=1, central_dns_suffix=CENTRAL_ENDPOINT, -) -> List[Device]: + cmd, + app_id: str, + token: str, + max_pages=1, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, +) -> List[Union[central_models.DevicePreview, central_models.DeviceV1]]: """ Get a list of all devices in IoTC app @@ -70,17 +93,28 @@ def list_devices( url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) headers = _utility.get_headers(token, cmd) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + pages_processed = 0 while (pages_processed <= max_pages) and url: - response = requests.get(url, headers=headers) + response = requests.get(url, headers=headers, params=query_parameters) result = _utility.try_extract_result(response) if "value" not in result: raise CLIError("Value is not present in body: {}".format(result)) - devices = devices + [Device(device) for device in result["value"]] + if api_version == ApiVersion.preview.value: + devices = devices + [ + central_models.DevicePreview(device) for device in result["value"] + ] + else: + devices = devices + [ + central_models.DeviceV1(device) for device in result["value"] + ] - url = result.get("nextLink") + url = result.get("nextLink", params=query_parameters) pages_processed = pages_processed + 1 return devices @@ -88,7 +122,7 @@ def list_devices( def get_device_registration_summary( cmd, app_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -): +) -> dict: """ Get device registration summary for a given app @@ -105,23 +139,32 @@ def get_device_registration_summary( registration_summary = {status.value: 0 for status in DeviceStatus} - url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) + url = "https://{}.{}/{}?api-version={}".format( + app_id, central_dns_suffix, BASE_PATH, ApiVersion.v1.value + ) headers = _utility.get_headers(token, cmd) + logger.warning( "This command may take a long time to complete if your app contains a lot of devices" ) + while url: - response = requests.get(url, headers=headers, verify=not should_disable_connection_verify()) + response = requests.get( + url, headers=headers, verify=not should_disable_connection_verify() + ) result = _utility.try_extract_result(response) if "value" not in result: raise CLIError("Value is not present in body: {}".format(result)) for device in result["value"]: - registration_summary[Device(device).device_status.value] += 1 + registration_summary[ + central_models.DeviceV1(device).device_status.value + ] += 1 print("Processed {} devices...".format(sum(registration_summary.values()))) url = result.get("nextLink") + return registration_summary @@ -130,11 +173,12 @@ def create_device( app_id: str, device_id: str, device_name: str, - instance_of: str, + template: str, simulated: bool, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -) -> Device: + api_version=ApiVersion.v1.value, +) -> Union[central_models.DevicePreview, central_models.DeviceV1]: """ Create a device in IoTC @@ -143,7 +187,7 @@ def create_device( app_id: name of app (used for forming request URL) device_id: unique case-sensitive device id device_name: (non-unique) human readable name for the device - instance_of: (optional) string that maps to the device_template_id + template: (optional) string that maps to the device_template_id of the device template that this device is to be an instance of simulated: if IoTC is to simulate data for this device token: (OPTIONAL) authorization token to fetch device details from IoTC. @@ -159,21 +203,44 @@ def create_device( url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, device_id) headers = _utility.get_headers(token, cmd, has_json_payload=True) - payload = { - "displayName": device_name, - "simulated": simulated, - "approved": True, - } - if instance_of: - payload["instanceOf"] = instance_of - - response = requests.put(url, headers=headers, json=payload) + + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + if api_version == ApiVersion.preview.value: + payload = { + "displayName": device_name, + "simulated": simulated, + "approved": True, + } + if template: + payload["instanceOf"] = template + else: + payload = { + "displayName": device_name, + "simulated": simulated, + "enabled": True, + } + if template: + payload["template"] = template + + response = requests.put(url, headers=headers, json=payload, params=query_parameters) result = _utility.try_extract_result(response) - return Device(result) + + if api_version == ApiVersion.preview.value: + return central_models.DevicePreview(result) + else: + return central_models.DeviceV1(result) def delete_device( - cmd, app_id: str, device_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + device_id: str, + token: str, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ) -> dict: """ Delete a device from IoTC @@ -193,12 +260,21 @@ def delete_device( url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, device_id) headers = _utility.get_headers(token, cmd) - response = requests.delete(url, headers=headers) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.delete(url, headers=headers, params=query_parameters) return _utility.try_extract_result(response) def get_device_credentials( - cmd, app_id: str, device_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, + app_id: str, + device_id: str, + token: str, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): """ Get device credentials from IoTC @@ -219,7 +295,11 @@ def get_device_credentials( ) headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers, verify=not should_disable_connection_verify()) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.get(url, headers=headers, params=query_parameters) return _utility.try_extract_result(response) @@ -232,6 +312,7 @@ def run_component_command( command_name: str, payload: dict, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): """ Execute a direct method on a device @@ -255,7 +336,13 @@ def run_component_command( ) headers = _utility.get_headers(token, cmd) - response = requests.post(url, headers=headers, json=payload, verify=not should_disable_connection_verify()) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.post( + url, headers=headers, json=payload, params=query_parameters + ) # execute command response has caveats in it due to Async/Sync device methods # return the response if we get 201, otherwise try to apply generic logic @@ -273,6 +360,7 @@ def get_component_command_history( interface_id: str, command_name: str, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ): """ Get component command history @@ -295,7 +383,11 @@ def get_component_command_history( ) headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers, verify=not should_disable_connection_verify()) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.get(url, headers=headers, params=query_parameters) return _utility.try_extract_result(response) @@ -310,7 +402,6 @@ def run_manual_failover( """ Execute a manual failover of device across multiple IoT Hubs to validate device firmware's ability to reconnect using DPS to a different IoT Hub. - Args: cmd: command passed into az app_id: id of an app (used for forming request URL) @@ -321,7 +412,6 @@ def run_manual_failover( token: (OPTIONAL) authorization token to fetch device details from IoTC. MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') central_dns_suffix:(OPTIONAL) {centralDnsSuffixInPath} as found in docs - Returns: result (currently a 200) """ @@ -331,28 +421,27 @@ def run_manual_failover( ) headers = _utility.get_headers(token, cmd) json = {} - if ttl_minutes : + if ttl_minutes: json = {"ttl": ttl_minutes} else: - print("""Using default time to live - - see https://github.com/iot-for-all/iot-central-high-availability-clients#readme for more information""") + print( + """Using default time to live - + see https://github.com/iot-for-all/iot-central-high-availability-clients#readme for more information""" + ) - response = requests.post(url, headers=headers, verify=not should_disable_connection_verify(), json=json) + response = requests.post( + url, headers=headers, verify=not should_disable_connection_verify(), json=json + ) _utility.log_response_debug(response=response, logger=logger) return _utility.try_extract_result(response) def run_manual_failback( - cmd, - app_id: str, - device_id: str, - token: str, - central_dns_suffix=CENTRAL_ENDPOINT, + cmd, app_id: str, device_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, ): """ Execute a manual failback for device. Reverts the previously executed failover command by moving the device back to it's original IoT Hub. - Args: cmd: command passed into az app_id: id of an app (used for forming request URL) @@ -360,7 +449,6 @@ def run_manual_failback( token: (OPTIONAL) authorization token to fetch device details from IoTC. MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') central_dns_suffix: {centralDnsSuffixInPath} as found in docs - Returns: result (currently a 200) """ @@ -369,7 +457,9 @@ def run_manual_failback( app_id, central_dns_suffix, "system/iothub/devices", device_id ) headers = _utility.get_headers(token, cmd) - response = requests.post(url, headers=headers, verify=not should_disable_connection_verify()) + response = requests.post( + url, headers=headers, verify=not should_disable_connection_verify() + ) _utility.log_response_debug(response=response, logger=logger) return _utility.try_extract_result(response) diff --git a/azext_iot/central/services/device_template.py b/azext_iot/central/services/device_template.py index 44d3a9590..7b8ec02e0 100644 --- a/azext_iot/central/services/device_template.py +++ b/azext_iot/central/services/device_template.py @@ -5,19 +5,20 @@ # -------------------------------------------------------------------------------------------- # This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devicetemplates +from typing import Union import requests - from typing import List from knack.util import CLIError from knack.log import get_logger from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central.services import _utility -from azext_iot.central.models.template import Template +from azext_iot.central import models as central_models +from azext_iot.central.models.enum import ApiVersion logger = get_logger(__name__) -BASE_PATH = "api/preview/deviceTemplates" +BASE_PATH = "api/deviceTemplates" def get_device_template( @@ -26,7 +27,8 @@ def get_device_template( device_template_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -) -> Template: + api_version=ApiVersion.v1.value, +) -> Union[central_models.TemplatePreview, central_models.TemplateV1]: """ Get a specific device template from IoTC @@ -46,13 +48,25 @@ def get_device_template( ) headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers) - return Template(_utility.try_extract_result(response)) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.get(url, headers=headers, params=query_parameters) + + if api_version == ApiVersion.preview.value: + return central_models.TemplatePreview(_utility.try_extract_result(response)) + else: + return central_models.TemplateV1(_utility.try_extract_result(response)) def list_device_templates( - cmd, app_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -) -> List[Template]: + cmd, + app_id: str, + token: str, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, +) -> List[Union[central_models.TemplatePreview, central_models.TemplateV1]]: """ Get a list of all device templates in IoTC @@ -70,14 +84,21 @@ def list_device_templates( url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.get(url, headers=headers, params=query_parameters) result = _utility.try_extract_result(response) if "value" not in result: raise CLIError("Value is not present in body: {}".format(result)) - return [Template(item) for item in result["value"]] + if api_version == ApiVersion.preview.value: + return [central_models.TemplatePreview(item) for item in result["value"]] + else: + return [central_models.TemplateV1(item) for item in result["value"]] def create_device_template( @@ -87,7 +108,8 @@ def create_device_template( payload: dict, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -) -> Template: + api_version=ApiVersion.v1.value, +) -> Union[central_models.TemplatePreview, central_models.TemplateV1]: """ Create a device template in IoTC @@ -112,8 +134,15 @@ def create_device_template( ) headers = _utility.get_headers(token, cmd, has_json_payload=True) - response = requests.put(url, headers=headers, json=payload) - return Template(_utility.try_extract_result(response)) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.put(url, headers=headers, json=payload, params=query_parameters) + if api_version == ApiVersion.preview.value: + return central_models.TemplatePreview(_utility.try_extract_result(response)) + else: + return central_models.TemplateV1(_utility.try_extract_result(response)) def delete_device_template( @@ -122,6 +151,7 @@ def delete_device_template( device_template_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, ) -> dict: """ Delete a device template from IoTC @@ -142,5 +172,9 @@ def delete_device_template( ) headers = _utility.get_headers(token, cmd) - response = requests.delete(url, headers=headers) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.delete(url, headers=headers, params=query_parameters) return _utility.try_extract_result(response) diff --git a/azext_iot/central/services/user.py b/azext_iot/central/services/user.py index 40b84987f..62eda8242 100644 --- a/azext_iot/central/services/user.py +++ b/azext_iot/central/services/user.py @@ -9,11 +9,11 @@ from knack.log import get_logger from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central.services import _utility -from azext_iot.central.models.enum import Role, UserType +from azext_iot.central.models.enum import Role, ApiVersion, UserTypePreview, UserTypeV1 logger = get_logger(__name__) -BASE_PATH = "api/preview/users" +BASE_PATH = "api/users" def add_service_principal( @@ -25,7 +25,8 @@ def add_service_principal( role: Role, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -): + api_version=ApiVersion.v1.value, +) -> dict: """ Add a user to a Central app @@ -43,16 +44,25 @@ def add_service_principal( """ url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, assignee) + if api_version == ApiVersion.v1.value: + user_type = UserTypeV1.service_principal.value + else: + user_type = UserTypePreview.service_principal.value + payload = { "tenantId": tenant_id, "objectId": object_id, - "type": UserType.service_principal.value, + "type": user_type, "roles": [{"role": role.value}], } headers = _utility.get_headers(token, cmd, has_json_payload=True) - response = requests.put(url, headers=headers, json=payload) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.put(url, headers=headers, json=payload, params=query_parameters) return _utility.try_extract_result(response) @@ -64,7 +74,8 @@ def add_email( role: Role, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -): + api_version=ApiVersion.v1.value, +) -> dict: """ Add a user to a Central app @@ -81,21 +92,34 @@ def add_email( """ url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, assignee) + if api_version == ApiVersion.v1.value: + user_type = UserTypeV1.email.value + else: + user_type = UserTypePreview.email.value + payload = { "email": email, - "type": UserType.email.value, + "type": user_type, "roles": [{"role": role.value}], } headers = _utility.get_headers(token, cmd, has_json_payload=True) - response = requests.put(url, headers=headers, json=payload) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.put(url, headers=headers, json=payload, params=query_parameters) return _utility.try_extract_result(response) def get_user_list( - cmd, app_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, -): + cmd, + app_id: str, + token: str, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, +) -> dict: """ Get the list of users for central app. @@ -113,13 +137,22 @@ def get_user_list( headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.get(url, headers=headers, params=query_parameters) return _utility.try_extract_result(response) def get_user( - cmd, app_id: str, token: str, assignee: str, central_dns_suffix=CENTRAL_ENDPOINT, -): + cmd, + app_id: str, + token: str, + assignee: str, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, +) -> dict: """ Get information for the specified user. @@ -138,13 +171,22 @@ def get_user( headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.get(url, headers=headers, params=query_parameters) return _utility.try_extract_result(response) def delete_user( - cmd, app_id: str, token: str, assignee: str, central_dns_suffix=CENTRAL_ENDPOINT, -): + cmd, + app_id: str, + token: str, + assignee: str, + central_dns_suffix=CENTRAL_ENDPOINT, + api_version=ApiVersion.v1.value, +) -> dict: """ delete user from theapp. @@ -163,5 +205,9 @@ def delete_user( headers = _utility.get_headers(token, cmd) - response = requests.delete(url, headers=headers) + # Construct parameters + query_parameters = {} + query_parameters["api-version"] = api_version + + response = requests.delete(url, headers=headers, params=query_parameters) return _utility.try_extract_result(response) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 68a1e021b..cd14f3cf5 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -40,51 +40,9 @@ def load_command_table(self, _): setter_name="iot_device_update", custom_func_name="update_iot_device_custom" ) - cmd_group.command("renew-key", 'iot_device_key_regenerate') - cmd_group.command( - "show-connection-string", - "iot_get_device_connection_string", - deprecate_info=self.deprecate( - redirect="az iot hub device-identity connection-string show" - ), - ) + cmd_group.command("renew-key", "iot_device_key_regenerate") cmd_group.command("import", "iot_device_import") cmd_group.command("export", "iot_device_export") - cmd_group.command( - "add-children", - "iot_device_children_add", - deprecate_info=self.deprecate( - redirect="az iot hub device-identity children add" - ), - ) - cmd_group.command( - "remove-children", - "iot_device_children_remove", - deprecate_info=self.deprecate( - redirect="az iot hub device-identity children remove" - ), - ) - cmd_group.command( - "list-children", - "iot_device_children_list_comma_separated", - deprecate_info=self.deprecate( - redirect="az iot hub device-identity children list" - ), - ) - cmd_group.command( - "get-parent", - "iot_device_get_parent", - deprecate_info=self.deprecate( - redirect="az iot hub device-identity parent show" - ), - ) - cmd_group.command( - "set-parent", - "iot_device_set_parent", - deprecate_info=self.deprecate( - redirect="az iot hub device-identity parent set" - ), - ) with self.command_group( "iot hub device-identity children", command_type=iothub_ops @@ -116,14 +74,7 @@ def load_command_table(self, _): getter_name="iot_device_module_show", setter_name="iot_device_module_update", ) - - cmd_group.show_command( - "show-connection-string", - "iot_get_module_connection_string", - deprecate_info=self.deprecate( - redirect="az iot hub module-identity connection-string show" - ), - ) + cmd_group.command("renew-key", "iot_device_module_key_regenerate") with self.command_group( "iot hub module-identity connection-string", command_type=iothub_ops diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index 2fd07e353..ce89fc919 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -56,6 +56,16 @@ class DeviceAuthType(Enum): x509_ca = "x509_ca" +class DeviceAuthApiType(Enum): + """ + Hub Device Authorization type. + """ + + sas = "sas" + selfSigned = "selfSigned" + certificateAuthority = "certificateAuthority" + + class KeyType(Enum): """ Shared private key. @@ -215,6 +225,15 @@ class AuthenticationType(Enum): identityBased = "identity" +class AuthenticationTypeDataplane(Enum): + """ + Use a policy key or Oauth token from Azure AD. + """ + + key = "key" + login = "login" + + class RenewKeyType(Enum): """ Target key type for regeneration. diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index e116f1d6d..fb632553a 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -349,10 +349,10 @@ def dict_transform_lower_case_key(d): return {k.lower(): v for k, v in d.items()} -def calculate_millisec_since_unix_epoch_utc(): +def calculate_millisec_since_unix_epoch_utc(offset_seconds: int = 0): now = datetime.utcnow() epoch = datetime.utcfromtimestamp(0) - return int(1000 * (now - epoch).total_seconds()) + return int(1000 * ((now - epoch).total_seconds() + offset_seconds)) def init_monitoring(cmd, timeout, properties, enqueued_time, repair, yes): diff --git a/azext_iot/constants.py b/azext_iot/constants.py index d4c932367..4f37a9004 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.11" +VERSION = "0.10.14" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" @@ -36,9 +36,10 @@ CENTRAL_ENDPOINT = "azureiotcentral.com" DEVICE_DEVICESCOPE_PREFIX = "ms-azure-iot-edge://" TRACING_PROPERTY = "azureiot*com^dtracing^1" -TRACING_ALLOWED_FOR_LOCATION = ("northeurope", "westus2", "west us 2", "southeastasia") +TRACING_ALLOWED_FOR_LOCATION = ("northeurope", "westus2", "southeastasia") TRACING_ALLOWED_FOR_SKU = "standard" USER_AGENT = "IoTPlatformCliExtension/{}".format(VERSION) +IOTHUB_RESOURCE_ID = "https://iothubs.azure.net" DIGITALTWINS_RESOURCE_ID = "https://digitaltwins.azure.net" DEVICETWIN_POLLING_INTERVAL_SEC = 10 DEVICETWIN_MONITOR_TIME_SEC = 15 @@ -50,4 +51,4 @@ CONFIG_KEY_UAMQP_EXT_VERSION = "uamqp_ext_version" # Initial Track 2 SDK version -IOTHUB_TRACK_2_SDK_MIN_VERSION = '1.0.0' +IOTHUB_TRACK_2_SDK_MIN_VERSION = '2.0.0' diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index a8ce93e48..8764b4719 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -113,6 +113,33 @@ def load_digitaltwins_help(): az dt delete -n {instance_name} -y --no-wait """ + helps["dt wait"] = """ + type: command + short-summary: Wait until an operation on an Digital Twins instance is complete. + + examples: + - name: Wait until an arbitrary instance is created. + text: > + az dt wait -n {instance_name} --created + - name: Wait until an existing instance is deleted. + text: > + az dt wait -n {instance_name} --deleted + - name: Wait until an existing instance's publicNetworkAccess property is set to Enabled + text: > + az dt wait -n {instance_name} --custom "publicNetworkAccess=='Enabled'" + """ + + helps["dt reset"] = """ + type: command + short-summary: Reset an existing Digital Twins instance by deleting associated + assets. Currently only supports deleting models and twins. + + examples: + - name: Reset all assets for a Digital Twins instance. + text: > + az dt reset -n {instance_name} + """ + helps["dt endpoint"] = """ type: group short-summary: Manage and configure Digital Twins instance endpoints. @@ -221,6 +248,22 @@ def load_digitaltwins_help(): az dt endpoint delete -n {instance_name} --endpoint-name {endpoint_name} -y --no-wait """ + helps["dt endpoint wait"] = """ + type: command + short-summary: Wait until an endpoint operation is done. + + examples: + - name: Wait until an endpoint for an instance is created. + text: > + az dt endpoint wait -n {instance_name} --endpoint-name {endpoint_name} --created + - name: Wait until an existing endpoint is deleted from an instance. + text: > + az dt endpoint wait -n {instance_name} --endpoint-name {endpoint_name} --deleted + - name: Wait until an existing endpoint's primaryConnectionString is null. + text: > + az dt endpoint wait -n {instance_name} --endpoint-name {endpoint_name} --custom "properties.primaryConnectionString==null" + """ + helps["dt network"] = """ type: group short-summary: Manage Digital Twins network configuration including private links and endpoint connections. @@ -290,7 +333,6 @@ def load_digitaltwins_help(): - name: Approve a pending private-endpoint connection associated with the instance and add a description. text: > az dt network private-endpoint connection set -n {instance_name} --cn {connection_name} --status Approved --desc "A description." - - name: Reject a private-endpoint connection associated with the instance and add a description. text: > az dt network private-endpoint connection set -n {instance_name} --cn {connection_name} --status Rejected --desc "Does not comply." @@ -310,6 +352,23 @@ def load_digitaltwins_help(): az dt network private-endpoint connection delete -n {instance_name} --cn ba8408b6-1372-41b2-aef8-af43afc4729f -y --no-wait """ + helps["dt network private-endpoint connection wait"] = """ + type: command + short-summary: Wait until an operation on a private-endpoint connection is complete. + + examples: + - name: Wait until the existing private-endpoint connection named ba8408b6-1372-41b2-aef8-af43afc4729f state is updated. + text: > + az dt network private-endpoint connection wait -n {instance_name} --cn ba8408b6-1372-41b2-aef8-af43afc4729f --updated + + - name: Wait until the existing private-endpoint connection named ba8408b6-1372-41b2-aef8-af43afc4729f is deleted. + text: > + az dt network private-endpoint connection wait -n {instance_name} --cn ba8408b6-1372-41b2-aef8-af43afc4729f --deleted + - name: Wait until the existing private-endpoint connection named ba8408b6-1372-41b2-aef8-af43afc4729f has no actions required in the privateLinkServiceConnectionState property. + text: > + az dt network private-endpoint connection wait -n {instance_name} --cn ba8408b6-1372-41b2-aef8-af43afc4729f --custom "properties.privateLinkServiceConnectionState.actionsRequired=='None'" + """ + helps["dt role-assignment"] = """ type: group short-summary: Manage RBAC role assignments for a Digital Twins instance. diff --git a/azext_iot/digitaltwins/command_map.py b/azext_iot/digitaltwins/command_map.py index da67ed879..f4e90eed5 100644 --- a/azext_iot/digitaltwins/command_map.py +++ b/azext_iot/digitaltwins/command_map.py @@ -42,6 +42,8 @@ def load_digitaltwins_commands(self, _): cmd_group.show_command("show", "show_instance") cmd_group.command("list", "list_instances") cmd_group.command("delete", "delete_instance", confirmation=True, supports_no_wait=True) + cmd_group.wait_command("wait", "wait_instance") + cmd_group.command("reset", "reset_instance", confirmation=True, is_preview=True) with self.command_group( "dt endpoint", command_type=digitaltwins_resource_ops @@ -63,6 +65,9 @@ def load_digitaltwins_commands(self, _): ), ) cmd_group.command("delete", "delete_endpoint", confirmation=True, supports_no_wait=True) + cmd_group.wait_command( + "wait", "wait_endpoint" + ) with self.command_group( "dt endpoint create", command_type=digitaltwins_resource_ops @@ -168,3 +173,6 @@ def load_digitaltwins_commands(self, _): cmd_group.show_command("show", "show_private_endpoint_conn") cmd_group.command("list", "list_private_endpoint_conns") cmd_group.command("delete", "delete_private_endpoint_conn", confirmation=True, supports_no_wait=True) + cmd_group.wait_command( + "wait", "wait_private_endpoint_conn" + ) diff --git a/azext_iot/digitaltwins/commands_resource.py b/azext_iot/digitaltwins/commands_resource.py index 96d176bc0..5847a196d 100644 --- a/azext_iot/digitaltwins/commands_resource.py +++ b/azext_iot/digitaltwins/commands_resource.py @@ -4,6 +4,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azext_iot.digitaltwins.commands_twins import delete_all_twin +from azext_iot.digitaltwins.commands_models import delete_all_models from azext_iot.digitaltwins.providers.resource import ResourceProvider from azext_iot.digitaltwins.common import ( ADTEndpointType, @@ -57,6 +59,16 @@ def delete_instance(cmd, name, resource_group_name=None): return rp.delete(name=name, resource_group_name=resource_group_name) +def wait_instance(cmd, name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.find_instance(name=name, resource_group_name=resource_group_name, wait=True) + + +def reset_instance(cmd, name, resource_group_name=None): + delete_all_models(cmd, name, resource_group_name) + delete_all_twin(cmd, name, resource_group_name) + + def list_endpoints(cmd, name, resource_group_name=None): rp = ResourceProvider(cmd) return rp.list_endpoints(name=name, resource_group_name=resource_group_name) @@ -76,6 +88,16 @@ def delete_endpoint(cmd, name, endpoint_name, resource_group_name=None): ) +def wait_endpoint(cmd, name, endpoint_name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.get_endpoint( + name=name, + endpoint_name=endpoint_name, + resource_group_name=resource_group_name, + wait=True + ) + + def add_endpoint_eventgrid( cmd, name, @@ -218,3 +240,13 @@ def delete_private_endpoint_conn(cmd, name, conn_name, resource_group_name=None) return rp.delete_private_endpoint_conn( name=name, resource_group_name=resource_group_name, conn_name=conn_name ) + + +def wait_private_endpoint_conn(cmd, name, conn_name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.get_private_endpoint_conn( + name=name, + resource_group_name=resource_group_name, + conn_name=conn_name, + wait=True + ) diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index 5214425b5..5842f284c 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -142,6 +142,9 @@ def load_digitaltwins_arguments(self, _): help="Role name or Id the system assigned identity will have.", ) + with self.argument_context("dt wait") as context: + context.ignore("updated") + with self.argument_context("dt endpoint create") as context: context.argument( "dead_letter_secret", @@ -249,6 +252,9 @@ def load_digitaltwins_arguments(self, _): arg_group="Service Bus Topic", ) + with self.argument_context("dt endpoint wait") as context: + context.ignore("updated") + with self.argument_context("dt twin") as context: context.argument( "query_command", @@ -423,3 +429,7 @@ def load_digitaltwins_arguments(self, _): help="A message indicating if changes on the service provider require any updates on the consumer.", arg_group="Private-Endpoint", ) + + with self.argument_context("dt network private-endpoint connection wait") as context: + context.ignore("created") + context.ignore("exists") diff --git a/azext_iot/digitaltwins/providers/auth.py b/azext_iot/digitaltwins/providers/auth.py index bb4cc528d..a6353e52c 100644 --- a/azext_iot/digitaltwins/providers/auth.py +++ b/azext_iot/digitaltwins/providers/auth.py @@ -9,7 +9,7 @@ class DigitalTwinAuthentication(Authentication): """ - Shared Access Signature authorization for Azure IoT Hub. + Azure AD OAuth for Azure Digital Twins. """ diff --git a/azext_iot/digitaltwins/providers/resource.py b/azext_iot/digitaltwins/providers/resource.py index f643764e7..958ae3287 100644 --- a/azext_iot/digitaltwins/providers/resource.py +++ b/azext_iot/digitaltwins/providers/resource.py @@ -117,20 +117,22 @@ def list_by_resouce_group(self, resource_group_name): except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) - def get(self, name, resource_group_name): + def get(self, name, resource_group_name, wait=False): try: return self.mgmt_sdk.digital_twins.get( resource_name=name, resource_group_name=resource_group_name ) except ErrorResponseException as e: + if wait: + e.status_code = e.response.status_code + raise e raise CLIError(unpack_msrest_error(e)) - def find_instance(self, name, resource_group_name=None): + def find_instance(self, name, resource_group_name=None, wait=False): if resource_group_name: - try: - return self.get(name=name, resource_group_name=resource_group_name) - except ErrorResponseException as e: - raise CLIError(unpack_msrest_error(e)) + return self.get( + name=name, resource_group_name=resource_group_name, wait=wait + ) dt_collection_pager = self.list() dt_collection = [] @@ -221,7 +223,7 @@ def remove_role(self, name, assignee, role_type=None, resource_group_name=None): # Endpoints - def get_endpoint(self, name, endpoint_name, resource_group_name=None): + def get_endpoint(self, name, endpoint_name, resource_group_name=None, wait=False): target_instance = self.find_instance( name=name, resource_group_name=resource_group_name ) @@ -235,6 +237,9 @@ def get_endpoint(self, name, endpoint_name, resource_group_name=None): resource_group_name=resource_group_name, ) except ErrorResponseException as e: + if wait: + e.status_code = e.response.status_code + raise e raise CLIError(unpack_msrest_error(e)) def list_endpoints(self, name, resource_group_name=None): @@ -417,7 +422,13 @@ def set_private_endpoint_conn( except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) - def get_private_endpoint_conn(self, name, conn_name, resource_group_name=None): + def get_private_endpoint_conn( + self, + name, + conn_name, + resource_group_name=None, + wait=False + ): target_instance = self.find_instance( name=name, resource_group_name=resource_group_name ) @@ -428,10 +439,12 @@ def get_private_endpoint_conn(self, name, conn_name, resource_group_name=None): return self.mgmt_sdk.private_endpoint_connections.get( resource_group_name=resource_group_name, resource_name=name, - private_endpoint_connection_name=conn_name, - raw=True, - ).response.json() + private_endpoint_connection_name=conn_name + ) except ErrorResponseException as e: + if wait: + e.status_code = e.response.status_code + raise e raise CLIError(unpack_msrest_error(e)) def list_private_endpoint_conns(self, name, resource_group_name=None): diff --git a/azext_iot/iothub/commands_job.py b/azext_iot/iothub/commands_job.py index 221f92508..520d49a4f 100644 --- a/azext_iot/iothub/commands_job.py +++ b/azext_iot/iothub/commands_job.py @@ -29,8 +29,15 @@ def job_create( hub_name=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): - jobs = JobProvider(cmd=cmd, hub_name=hub_name, rg=resource_group_name, login=login) + jobs = JobProvider( + cmd=cmd, + hub_name=hub_name, + rg=resource_group_name, + login=login, + auth_type_dataplane=auth_type_dataplane, + ) return jobs.create( job_id=job_id, job_type=job_type, @@ -48,8 +55,21 @@ def job_create( ) -def job_show(cmd, job_id, hub_name=None, resource_group_name=None, login=None): - jobs = JobProvider(cmd=cmd, hub_name=hub_name, rg=resource_group_name, login=login) +def job_show( + cmd, + job_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, +): + jobs = JobProvider( + cmd=cmd, + hub_name=hub_name, + rg=resource_group_name, + login=login, + auth_type_dataplane=auth_type_dataplane, + ) return jobs.get(job_id) @@ -61,11 +81,31 @@ def job_list( hub_name=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): - jobs = JobProvider(cmd=cmd, hub_name=hub_name, rg=resource_group_name, login=login) + jobs = JobProvider( + cmd=cmd, + hub_name=hub_name, + rg=resource_group_name, + login=login, + auth_type_dataplane=auth_type_dataplane, + ) return jobs.list(job_type=job_type, job_status=job_status, top=top) -def job_cancel(cmd, job_id, hub_name=None, resource_group_name=None, login=None): - jobs = JobProvider(cmd=cmd, hub_name=hub_name, rg=resource_group_name, login=login) +def job_cancel( + cmd, + job_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, +): + jobs = JobProvider( + cmd=cmd, + hub_name=hub_name, + rg=resource_group_name, + login=login, + auth_type_dataplane=auth_type_dataplane, + ) return jobs.cancel(job_id) diff --git a/azext_iot/iothub/providers/aad_oauth.py b/azext_iot/iothub/providers/aad_oauth.py new file mode 100644 index 000000000..174f2c571 --- /dev/null +++ b/azext_iot/iothub/providers/aad_oauth.py @@ -0,0 +1,62 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from msrest.authentication import Authentication + + +class IoTHubOAuth(Authentication): + """ + Azure AD OAuth for Azure IoT Hub. + + """ + + def __init__(self, cmd, resource_id): + self.resource_id = resource_id + self.cmd = cmd + + def signed_session(self, session=None): + """ + Create requests session with SAS auth headers. + + If a session object is provided, configure it directly. Otherwise, + create a new session and return it. + + Returns: + session (): requests.Session. + """ + + return self.refresh_session(session) + + def refresh_session( + self, session=None, + ): + """ + Refresh requests session with SAS auth headers. + + If a session object is provided, configure it directly. Otherwise, + create a new session and return it. + + Returns: + session (): requests.Session. + """ + + session = session or super(IoTHubOAuth, self).signed_session() + session.headers["Authorization"] = self.generate_token() + return session + + def generate_token(self): + from azure.cli.core._profile import Profile + + profile = Profile(cli_ctx=self.cmd.cli_ctx) + creds, subscription, tenant = profile.get_raw_token(resource=self.resource_id) + parsed_token = { + "tokenType": creds[0], + "accessToken": creds[1], + "expiresOn": creds[2].get("expiresOn", "N/A"), + "subscription": subscription, + "tenant": tenant, + } + return "{} {}".format(parsed_token["tokenType"], parsed_token["accessToken"]) diff --git a/azext_iot/iothub/providers/base.py b/azext_iot/iothub/providers/base.py index e668264dc..4d1ac7fe9 100644 --- a/azext_iot/iothub/providers/base.py +++ b/azext_iot/iothub/providers/base.py @@ -14,7 +14,7 @@ class IoTHubProvider(object): - def __init__(self, cmd, hub_name, rg, login=None): + def __init__(self, cmd, hub_name, rg, login=None, auth_type_dataplane=None): self.cmd = cmd self.hub_name = hub_name self.rg = rg @@ -23,6 +23,7 @@ def __init__(self, cmd, hub_name, rg, login=None): hub_name=self.hub_name, resource_group_name=self.rg, login=login, + auth_type=auth_type_dataplane, ) self.resolver = SdkResolver(self.target) diff --git a/azext_iot/iothub/providers/discovery.py b/azext_iot/iothub/providers/discovery.py index 4d9043d4a..93ecdcfa1 100644 --- a/azext_iot/iothub/providers/discovery.py +++ b/azext_iot/iothub/providers/discovery.py @@ -8,10 +8,12 @@ from knack.log import get_logger from azure.cli.core.commands.client_factory import get_subscription_id from azext_iot.common.utility import trim_from_start, ensure_iothub_sdk_min_version +from azext_iot.common.shared import AuthenticationTypeDataplane from azext_iot.iothub.models.iothub_target import IotHubTarget from azext_iot._factory import iot_hub_service_factory from azext_iot.constants import IOTHUB_TRACK_2_SDK_MIN_VERSION from typing import Dict, List +from types import SimpleNamespace from enum import Enum, EnumMeta PRIVILEDGED_ACCESS_RIGHTS_SET = set( @@ -142,8 +144,29 @@ def get_target(self, hub_name: str, resource_group_name: str = None, **kwargs) - if cstring: return self.get_target_by_cstring(connection_string=cstring) + resource_group_name = resource_group_name or kwargs.get("rg") + target_iothub = self.find_iothub(hub_name=hub_name, rg=resource_group_name) + key_type = kwargs.get("key_type", "primary") + include_events = kwargs.get("include_events", False) + + # Azure AD auth path + auth_type = kwargs.get("auth_type", AuthenticationTypeDataplane.key.value) + if auth_type == AuthenticationTypeDataplane.login.value: + logger.info("Using AAD access token for IoT Hub interaction.") + policy = SimpleNamespace() + policy.key_name = AuthenticationTypeDataplane.login.value + policy.primary_key = AuthenticationTypeDataplane.login.value + policy.secondary_key = AuthenticationTypeDataplane.login.value + + return self._build_target( + iothub=target_iothub, + policy=policy, + key_type="primary", + include_events=include_events + ) + policy_name = kwargs.get("policy_name", "auto") rg = target_iothub.additional_properties.get("resourcegroup") @@ -151,8 +174,6 @@ def get_target(self, hub_name: str, resource_group_name: str = None, **kwargs) - hub_name=target_iothub.name, rg=rg, policy_name=policy_name, ) - key_type = kwargs.get("key_type", "primary") - include_events = kwargs.get("include_events", False) return self._build_target( iothub=target_iothub, policy=target_policy, @@ -211,4 +232,6 @@ def _build_target( ].partition_ids target["events"] = events + target["cmd"] = self.cmd + return target diff --git a/azext_iot/monitor/event.py b/azext_iot/monitor/event.py index b72271a8d..9c00fa684 100644 --- a/azext_iot/monitor/event.py +++ b/azext_iot/monitor/event.py @@ -8,11 +8,14 @@ import uamqp import yaml +from typing import Tuple, Union from uuid import uuid4 from knack.log import get_logger from azext_iot.constants import USER_AGENT +from azext_iot.common.shared import AuthenticationTypeDataplane from azext_iot.common.utility import process_json_arg from azext_iot.monitor.builders.hub_target_builder import AmqpBuilder +from uamqp.authentication import JWTTokenAuth # To provide amqp frame trace DEBUG = False @@ -56,7 +59,7 @@ def send_c2d_message( # Ensures valid json when content_type is application/json content_type = content_type.lower() - if content_type == "application/json": + if "application/json" in content_type: data = json.dumps(process_json_arg(data, "data")) if content_encoding: @@ -72,10 +75,13 @@ def send_c2d_message( ) operation = "/messages/devicebound" - endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target(target) - endpoint_with_op = endpoint + operation + endpoint_target, token_auth = _get_endpoint_and_token_auth( + target=target, operation=operation + ) + client = uamqp.SendClient( - target="amqps://" + endpoint_with_op, + target=endpoint_target, + auth=token_auth, client_name=_get_container_id(), debug=DEBUG, ) @@ -107,24 +113,23 @@ def handle_msg(msg): return None operation = "/messages/servicebound/feedback" - endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target( - target, duration=token_duration + endpoint_target, token_auth = _get_endpoint_and_token_auth( + target=target, operation=operation ) - endpoint = endpoint + operation - device_filter_txt = None if device_id: device_filter_txt = " filtering on device: {},".format(device_id) print( - "Starting C2D feedback monitor,{} use ctrl-c to stop...".format( - device_filter_txt if device_filter_txt else "" - ) + f"Starting C2D feedback monitor,{device_filter_txt if device_filter_txt else ''} use ctrl-c to stop..." ) try: client = uamqp.ReceiveClient( - "amqps://" + endpoint, client_name=_get_container_id(), debug=DEBUG + source=endpoint_target, + auth=token_auth, + client_name=_get_container_id(), + debug=DEBUG, ) message_generator = client.receive_messages_iter() for msg in message_generator: @@ -141,3 +146,36 @@ def handle_msg(msg): def _get_container_id(): return "{}/{}".format(USER_AGENT, str(uuid4())) + + +def _get_endpoint_and_token_auth( + target: dict, operation: str +) -> Tuple[str, Union[JWTTokenAuth, None]]: + from azext_iot.constants import IOTHUB_RESOURCE_ID + from time import time + from collections import namedtuple + + AccessToken = namedtuple("AccessToken", ["token", "expires_on"]) + + def token_provider(): + from azure.cli.core._profile import Profile + profile = Profile(cli_ctx=target["cmd"].cli_ctx) + creds, _, _ = profile.get_raw_token(resource=IOTHUB_RESOURCE_ID) + access_token = AccessToken(f"{creds[0]} {creds[1]}", time() + 3599) + return access_token + + endpoint_with_op = None + jwt_token_auth = None + if target["policy"] == AuthenticationTypeDataplane.login.value: + endpoint_with_op = f"amqps://{target['entity']}{operation}" + jwt_token_auth = JWTTokenAuth( + audience=IOTHUB_RESOURCE_ID, + uri=endpoint_with_op, + get_token=token_provider, + token_type=b"Bearer", + ) + jwt_token_auth.update_token() # Work-around for uamqp error. + else: + endpoint_with_op = f"amqps://{AmqpBuilder.build_iothub_amqp_endpoint_from_target(target)}{operation}" + + return endpoint_with_op, jwt_token_auth diff --git a/azext_iot/monitor/handlers/central_handler.py b/azext_iot/monitor/handlers/central_handler.py index 54311ff1c..07d68b588 100644 --- a/azext_iot/monitor/handlers/central_handler.py +++ b/azext_iot/monitor/handlers/central_handler.py @@ -11,9 +11,9 @@ from knack.log import get_logger from azext_iot.monitor.utility import stop_monitor, get_loop -from azext_iot.central.providers import ( - CentralDeviceProvider, - CentralDeviceTemplateProvider, +from azext_iot.central.providers.v1 import ( + CentralDeviceProviderV1, + CentralDeviceTemplateProviderV1, ) from azext_iot.monitor.handlers import CommonHandler from azext_iot.monitor.models.arguments import CentralHandlerArguments @@ -26,8 +26,8 @@ class CentralHandler(CommonHandler): def __init__( self, - central_device_provider: CentralDeviceProvider, - central_template_provider: CentralDeviceTemplateProvider, + central_device_provider: CentralDeviceProviderV1, + central_template_provider: CentralDeviceTemplateProviderV1, central_handler_args: CentralHandlerArguments, ): super(CentralHandler, self).__init__( diff --git a/azext_iot/monitor/parsers/central_parser.py b/azext_iot/monitor/parsers/central_parser.py index f5a8c4686..bd719492d 100644 --- a/azext_iot/monitor/parsers/central_parser.py +++ b/azext_iot/monitor/parsers/central_parser.py @@ -8,11 +8,11 @@ from uamqp.message import Message -from azext_iot.central.providers import ( - CentralDeviceProvider, - CentralDeviceTemplateProvider, +from azext_iot.central.providers.v1 import ( + CentralDeviceProviderV1, + CentralDeviceTemplateProviderV1, ) -from azext_iot.central.models.template import Template +from azext_iot.central import models as central_models from azext_iot.monitor.parsers import strings from azext_iot.monitor.central_validator import validate, extract_schema_type from azext_iot.monitor.models.arguments import CommonParserArguments @@ -25,8 +25,8 @@ def __init__( self, message: Message, common_parser_args: CommonParserArguments, - central_device_provider: CentralDeviceProvider, - central_template_provider: CentralDeviceTemplateProvider, + central_device_provider: CentralDeviceProviderV1, + central_template_provider: CentralDeviceTemplateProviderV1, ): super(CentralParser, self).__init__( message=message, common_parser_args=common_parser_args @@ -89,7 +89,7 @@ def _perform_dynamic_validations(self, payload: dict): template = self._get_template() - if not isinstance(template, Template): + if not isinstance(template, central_models.TemplateV1): return # if component name is not defined then data should be mapped to root/inherited interfaces @@ -120,7 +120,7 @@ def _get_template(self): try: device = self._central_device_provider.get_device(self.device_id) template = self._central_template_provider.get_device_template( - device.instance_of + device.template ) self._template_id = template.id return template @@ -131,7 +131,9 @@ def _get_template(self): # currently validates: # 1) primitive types match (e.g. boolean is indeed bool etc) # 2) names match (i.e. Humidity vs humidity etc) - def _validate_payload(self, payload: dict, template: Template, is_component: bool): + def _validate_payload( + self, payload: dict, template: central_models.TemplateV1, is_component: bool + ): name_miss = [] for telemetry_name, telemetry in payload.items(): schema = template.get_schema( diff --git a/azext_iot/monitor/property.py b/azext_iot/monitor/property.py index b91a7e889..4fc62bb15 100644 --- a/azext_iot/monitor/property.py +++ b/azext_iot/monitor/property.py @@ -17,10 +17,11 @@ ) from azext_iot.central.models.devicetwin import DeviceTwin, Property -from azext_iot.central.providers import ( - CentralDeviceProvider, - CentralDeviceTemplateProvider, - CentralDeviceTwinProvider, + +from azext_iot.central.providers import CentralDeviceTwinProvider +from azext_iot.central.providers.v1 import ( + CentralDeviceProviderV1, + CentralDeviceTemplateProviderV1, ) from azext_iot.monitor.parsers.issue import IssueHandler @@ -45,10 +46,10 @@ def __init__( token=self._token, device_id=self._device_id, ) - self._central_device_provider = CentralDeviceProvider( + self._central_device_provider = CentralDeviceProviderV1( cmd=self._cmd, app_id=self._app_id, token=self._token ) - self._central_template_provider = CentralDeviceTemplateProvider( + self._central_template_provider = CentralDeviceTemplateProviderV1( cmd=self._cmd, app_id=self._app_id, token=self._token ) self._template = self._get_device_template() @@ -162,7 +163,7 @@ def _validate_payload_against_entities(self, payload: dict, name, minimum_severi def _get_device_template(self): device = self._central_device_provider.get_device(self._device_id) template = self._central_template_provider.get_device_template( - device_template_id=device.instance_of, + device_template_id=device.template, central_dns_suffix=self._central_dns_suffix, ) return template diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 9fba449cd..6f08f7f9f 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -14,6 +14,7 @@ TRACING_PROPERTY, TRACING_ALLOWED_FOR_LOCATION, TRACING_ALLOWED_FOR_SKU, + IOTHUB_TRACK_2_SDK_MIN_VERSION, ) from azext_iot.common.sas_token_auth import SasTokenAuthentication from azext_iot.common.shared import ( @@ -24,7 +25,8 @@ KeyType, SettleType, RenewKeyType, - IoTHubStateType + IoTHubStateType, + DeviceAuthApiType, ) from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot.common.utility import ( @@ -35,7 +37,7 @@ init_monitoring, process_json_arg, ensure_iothub_sdk_min_version, - generate_key + generate_key, ) from azext_iot._factory import SdkResolver, CloudError from azext_iot.operations.generic import _execute_query, _process_top @@ -48,12 +50,21 @@ # Query def iot_query( - cmd, query_command, hub_name=None, top=None, resource_group_name=None, login=None + cmd, + query_command, + hub_name=None, + top=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): top = _process_top(top) discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -71,11 +82,19 @@ def iot_query( def iot_device_show( - cmd, device_id, hub_name=None, resource_group_name=None, login=None + cmd, + device_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) return _iot_device_show(target, device_id) @@ -101,13 +120,23 @@ def iot_device_list( edge_enabled=False, resource_group_name=None, login=None, + auth_type_dataplane=None, ): query = ( "select * from devices where capabilities.iotEdge = true" if edge_enabled else "select * from devices" ) - result = iot_query(cmd, query, hub_name, top, resource_group_name, login=login) + result = iot_query( + cmd=cmd, + query_command=query, + hub_name=hub_name, + top=top, + resource_group_name=resource_group_name, + login=login, + auth_type_dataplane=auth_type_dataplane, + ) + if not result: logger.info('No registered devices found on hub "%s".', hub_name) return result @@ -125,36 +154,20 @@ def iot_device_create( status_reason=None, valid_days=None, output_dir=None, - set_parent_id=None, - add_children=None, - force=False, resource_group_name=None, login=None, + auth_type_dataplane=None, ): - discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) - if add_children: - if not edge_enabled: - raise CLIError( - 'The device "{}" should be edge device in order to add children.'.format(device_id) - ) - - for child_device_id in add_children.split(","): - child_device = _iot_device_show(target, child_device_id.strip()) - _validate_parent_child_relation(child_device, force) - - deviceScope = None - if set_parent_id: - edge_device = _iot_device_show(target, set_parent_id) - _validate_edge_device(edge_device) - deviceScope = edge_device["deviceScope"] - if any([valid_days, output_dir]): valid_days = 365 if not valid_days else int(valid_days) if output_dir and not exists(output_dir): @@ -176,7 +189,6 @@ def iot_device_create( secondary_thumbprint, status, status_reason, - deviceScope ) output = service_sdk.devices.create_or_update_identity( id=device_id, device=device @@ -184,11 +196,6 @@ def iot_device_create( except CloudError as e: raise CLIError(unpack_msrest_error(e)) - if add_children: - for child_device_id in add_children.split(","): - child_device = _iot_device_show(target, child_device_id.strip()) - _update_device_parent(target, child_device, child_device["capabilities"]["iotEdge"], output.device_scope) - return output @@ -250,21 +257,21 @@ def _assemble_auth(auth_method, pk, sk): ) auth = None - if auth_method in [DeviceAuthType.shared_private_key.name, "sas"]: + if auth_method in [DeviceAuthType.shared_private_key.name, DeviceAuthApiType.sas.value]: auth = AuthenticationMechanism( - symmetric_key=SymmetricKey(primary_key=pk, secondary_key=sk), type="sas" + symmetric_key=SymmetricKey(primary_key=pk, secondary_key=sk), type=DeviceAuthApiType.sas.value ) - elif auth_method in [DeviceAuthType.x509_thumbprint.name, "selfSigned"]: + elif auth_method in [DeviceAuthType.x509_thumbprint.name, DeviceAuthApiType.selfSigned.value]: if not pk: raise ValueError("primary thumbprint required with selfSigned auth") auth = AuthenticationMechanism( x509_thumbprint=X509Thumbprint( primary_thumbprint=pk, secondary_thumbprint=sk ), - type="selfSigned", + type=DeviceAuthApiType.selfSigned.value, ) - elif auth_method in [DeviceAuthType.x509_ca.name, "certificateAuthority"]: - auth = AuthenticationMechanism(type="certificateAuthority") + elif auth_method in [DeviceAuthType.x509_ca.name, DeviceAuthApiType.certificateAuthority.value]: + auth = AuthenticationMechanism(type=DeviceAuthApiType.certificateAuthority.value) else: raise ValueError("Authorization method {} invalid.".format(auth_method)) return auth @@ -294,10 +301,10 @@ def update_iot_device_custom( if status_reason is not None: instance["statusReason"] = status_reason - auth_type = instance['authentication']['type'] + auth_type = instance["authentication"]["type"] if auth_method is not None: if auth_method == DeviceAuthType.shared_private_key.name: - auth = "sas" + auth = DeviceAuthApiType.sas.value if (primary_key and not secondary_key) or ( not primary_key and secondary_key ): @@ -305,7 +312,7 @@ def update_iot_device_custom( instance["authentication"]["symmetricKey"]["primaryKey"] = primary_key instance["authentication"]["symmetricKey"]["secondaryKey"] = secondary_key elif auth_method == DeviceAuthType.x509_thumbprint.name: - auth = "selfSigned" + auth = DeviceAuthApiType.selfSigned.value if not any([primary_thumbprint, secondary_thumbprint]): raise CLIError( "primary or secondary Thumbprint required with selfSigned auth" @@ -319,13 +326,13 @@ def update_iot_device_custom( "secondaryThumbprint" ] = secondary_thumbprint elif auth_method == DeviceAuthType.x509_ca.name: - auth = "certificateAuthority" + auth = DeviceAuthApiType.certificateAuthority.value else: raise ValueError("Authorization method {} invalid.".format(auth_method)) instance["authentication"]["type"] = auth # if no new auth_method is provided, validate secondary auth arguments and update accordingly - elif auth_type == "sas": + elif auth_type == DeviceAuthApiType.sas.value: if any([primary_thumbprint, secondary_thumbprint]): raise ValueError( "Device authorization method {} does not support primary or secondary thumbprints.".format( @@ -337,7 +344,7 @@ def update_iot_device_custom( if secondary_key: instance["authentication"]["symmetricKey"]["secondaryKey"] = secondary_key - elif auth_type == "selfSigned": + elif auth_type == DeviceAuthApiType.selfSigned.value: if any([primary_key, secondary_key]): raise ValueError( "Device authorization method {} does not support primary or secondary keys.".format( @@ -356,24 +363,34 @@ def update_iot_device_custom( def iot_device_update( - cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None, etag=None + cmd, + device_id, + parameters, + hub_name=None, + resource_group_name=None, + login=None, + etag=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) auth, pk, sk = _parse_auth(parameters) updated_device = _assemble_device( True, - parameters['deviceId'], + parameters["deviceId"], auth, - parameters['capabilities']['iotEdge'], + parameters["capabilities"]["iotEdge"], pk, sk, - parameters['status'].lower(), - parameters.get('statusReason'), - parameters.get('deviceScope') + parameters["status"].lower(), + parameters.get("statusReason"), + parameters.get("deviceScope"), ) updated_device.etag = etag if etag else "*" return _iot_device_update(target, device_id, updated_device) @@ -387,20 +404,27 @@ def _iot_device_update(target, device_id, device): headers = {} headers["If-Match"] = '"{}"'.format(device.etag) return service_sdk.devices.create_or_update_identity( - id=device_id, - device=device, - custom_headers=headers + id=device_id, device=device, custom_headers=headers ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) def iot_device_delete( - cmd, device_id, hub_name=None, resource_group_name=None, login=None, etag=None + cmd, + device_id, + hub_name=None, + resource_group_name=None, + login=None, + etag=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -408,9 +432,7 @@ def iot_device_delete( try: headers = {} headers["If-Match"] = '"{}"'.format(etag if etag else "*") - service_sdk.devices.delete_identity( - id=device_id, custom_headers=headers - ) + service_sdk.devices.delete_identity(id=device_id, custom_headers=headers) return except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -434,13 +456,25 @@ def _update_device_key(target, device, auth_method, pk, sk, etag=None): raise CLIError(unpack_msrest_error(e)) -def iot_device_key_regenerate(cmd, hub_name, device_id, renew_key_type, resource_group_name=None, login=None, etag=None): +def iot_device_key_regenerate( + cmd, + hub_name, + device_id, + renew_key_type, + resource_group_name=None, + login=None, + etag=None, + auth_type_dataplane=None, +): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) device = _iot_device_show(target, device_id) - if (device["authentication"]["type"] != "sas"): + if device["authentication"]["type"] != DeviceAuthApiType.sas.value: raise CLIError("Device authentication should be of type sas") pk = device["authentication"]["symmetricKey"]["primaryKey"] @@ -455,15 +489,25 @@ def iot_device_key_regenerate(cmd, hub_name, device_id, renew_key_type, resource pk = sk sk = temp - return _update_device_key(target, device, device["authentication"]["type"], pk, sk, etag) + return _update_device_key( + target, device, device["authentication"]["type"], pk, sk, etag + ) def iot_device_get_parent( - cmd, device_id, hub_name=None, resource_group_name=None, login=None + cmd, + device_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) child_device = _iot_device_show(target, device_id) _validate_child_device(child_device) @@ -482,17 +526,26 @@ def iot_device_set_parent( hub_name=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) parent_device = _iot_device_show(target, parent_id) _validate_edge_device(parent_device) child_device = _iot_device_show(target, device_id) _validate_parent_child_relation(child_device, force) - _update_device_parent(target, child_device, child_device["capabilities"]["iotEdge"], parent_device["deviceScope"]) + _update_device_parent( + target, + child_device, + child_device["capabilities"]["iotEdge"], + parent_device["deviceScope"], + ) def iot_device_children_add( @@ -503,26 +556,31 @@ def iot_device_children_add( hub_name=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) devices = [] edge_device = _iot_device_show(target, device_id) _validate_edge_device(edge_device) converted_child_list = child_list - if isinstance(child_list, str): # this check would be removed once add-children command is deprecated - converted_child_list = child_list.split(",") for child_device_id in converted_child_list: child_device = _iot_device_show(target, child_device_id.strip()) - _validate_parent_child_relation( - child_device, force - ) + _validate_parent_child_relation(child_device, force) devices.append(child_device) for device in devices: - _update_device_parent(target, device, device["capabilities"]["iotEdge"], edge_device["deviceScope"]) + _update_device_parent( + target, + device, + device["capabilities"]["iotEdge"], + edge_device["deviceScope"], + ) def iot_device_children_remove( @@ -533,10 +591,14 @@ def iot_device_children_remove( hub_name=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) devices = [] if remove_all: @@ -556,8 +618,6 @@ def iot_device_children_remove( edge_device = _iot_device_show(target, device_id) _validate_edge_device(edge_device) converted_child_list = child_list - if isinstance(child_list, str): # this check would be removed once remove-children command is deprecated - converted_child_list = child_list.split(",") for child_device_id in converted_child_list: child_device = _iot_device_show(target, child_device_id.strip()) _validate_child_device(child_device) @@ -579,42 +639,58 @@ def iot_device_children_remove( def iot_device_children_list( - cmd, device_id, hub_name=None, resource_group_name=None, login=None + cmd, + device_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): result = _iot_device_children_list( - cmd, device_id, hub_name, resource_group_name, login + cmd=cmd, + device_id=device_id, + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type_dataplane=auth_type_dataplane, ) return [device["deviceId"] for device in result] -# this method would be removed once remove-children command is deprecated -def iot_device_children_list_comma_separated( - cmd, device_id, hub_name=None, resource_group_name=None, login=None -): - result = _iot_device_children_list( - cmd, device_id, hub_name, resource_group_name, login - ) - if not result: - raise CLIError( - 'No registered child devices found for "{}" edge device.'.format(device_id) - ) - return ", ".join([str(x["deviceId"]) for x in result]) - - def _iot_device_children_list( - cmd, device_id, hub_name=None, resource_group_name=None, login=None + cmd, + device_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) device = _iot_device_show(target, device_id) _validate_edge_device(device) - query = "select deviceId from devices where array_contains(parentScopes, '{}')".format( - device["deviceScope"] + query = ( + "select deviceId from devices where array_contains(parentScopes, '{}')".format( + device["deviceScope"] + ) + ) + + # TODO: Inefficient + return iot_query( + cmd=cmd, + query_command=query, + hub_name=hub_name, + top=None, + resource_group_name=resource_group_name, + login=login, + auth_type_dataplane=auth_type_dataplane, ) - return iot_query(cmd, query, hub_name, None, resource_group_name, login=login) def _update_device_parent(target, device, is_edge, device_scope=None): @@ -664,9 +740,7 @@ def _validate_child_device(device): ) if not device["parentScopes"]: raise CLIError( - 'Device "{}" doesn\'t have any parent device.'.format( - device["deviceId"] - ) + 'Device "{}" doesn\'t have any parent device.'.format(device["deviceId"]) ) @@ -697,6 +771,7 @@ def iot_device_module_create( output_dir=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): if any([valid_days, output_dir]): @@ -712,7 +787,10 @@ def iot_device_module_create( discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -749,10 +827,14 @@ def iot_device_module_update( resource_group_name=None, login=None, etag=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -783,15 +865,15 @@ def _handle_module_update_params(parameters): def _parse_auth(parameters): - valid_auth = ["sas", "selfSigned", "certificateAuthority"] + valid_auth = [DeviceAuthApiType.sas.value, DeviceAuthApiType.selfSigned.value, DeviceAuthApiType.certificateAuthority.value] auth = parameters["authentication"].get("type") if auth not in valid_auth: raise CLIError("authentication.type must be one of {}".format(valid_auth)) pk = sk = None - if auth == "sas": + if auth == DeviceAuthApiType.sas.value: pk = parameters["authentication"]["symmetricKey"]["primaryKey"] sk = parameters["authentication"]["symmetricKey"]["secondaryKey"] - elif auth == "selfSigned": + elif auth == DeviceAuthApiType.selfSigned.value: pk = parameters["authentication"]["x509Thumbprint"]["primaryThumbprint"] sk = parameters["authentication"]["x509Thumbprint"]["secondaryThumbprint"] if not any([pk, sk]): @@ -801,12 +883,79 @@ def _parse_auth(parameters): return auth, pk, sk +def iot_device_module_key_regenerate( + cmd, + hub_name, + device_id, + module_id, + renew_key_type, + resource_group_name=None, + login=None, + etag=None, + auth_type_dataplane=None, +): + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, + ) + resolver = SdkResolver(target=target) + service_sdk = resolver.get_sdk(SdkType.service_sdk) + try: + module = service_sdk.modules.get_identity( + id=device_id, mid=module_id, raw=True + ).response.json() + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + if module["authentication"]["type"] != "sas": + raise CLIError("Module authentication should be of type sas") + + pk = module["authentication"]["symmetricKey"]["primaryKey"] + sk = module["authentication"]["symmetricKey"]["secondaryKey"] + + if renew_key_type == RenewKeyType.primary.value: + pk = generate_key() + if renew_key_type == RenewKeyType.secondary.value: + sk = generate_key() + if renew_key_type == RenewKeyType.swap.value: + temp = pk + pk = sk + sk = temp + + module["authentication"]["symmetricKey"]["primaryKey"] = pk + module["authentication"]["symmetricKey"]["secondaryKey"] = sk + + try: + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + return service_sdk.modules.create_or_update_identity( + id=device_id, + mid=module_id, + module=module, + custom_headers=headers, + ) + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def iot_device_module_list( - cmd, device_id, hub_name=None, top=1000, resource_group_name=None, login=None + cmd, + device_id, + hub_name=None, + top=1000, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -818,11 +967,20 @@ def iot_device_module_list( def iot_device_module_show( - cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None + cmd, + device_id, + module_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) return _iot_device_module_show(target, device_id, module_id) @@ -842,11 +1000,21 @@ def _iot_device_module_show(target, device_id, module_id): def iot_device_module_delete( - cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None, etag=None + cmd, + device_id, + module_id, + hub_name=None, + resource_group_name=None, + login=None, + etag=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -863,11 +1031,20 @@ def iot_device_module_delete( def iot_device_module_twin_show( - cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None + cmd, + device_id, + module_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) return _iot_device_module_twin_show( target=target, device_id=device_id, module_id=module_id @@ -894,13 +1071,17 @@ def iot_device_module_twin_update( hub_name=None, resource_group_name=None, login=None, - etag=None + etag=None, + auth_type_dataplane=None, ): from azext_iot.common.utility import verify_transform discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -935,11 +1116,15 @@ def iot_device_module_twin_replace( hub_name=None, resource_group_name=None, login=None, - etag=None + etag=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -959,13 +1144,22 @@ def iot_device_module_twin_replace( def iot_edge_set_modules( - cmd, device_id, content, hub_name=None, resource_group_name=None, login=None + cmd, + device_id, + content, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): from azext_iot.sdk.iothub.service.models import ConfigurationContent discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -996,6 +1190,7 @@ def iot_edge_deployment_create( no_validation=False, resource_group_name=None, login=None, + auth_type_dataplane=None, ): # Short-term fix for --no-validation config_type = ConfigType.layered if layered or no_validation else ConfigType.edge @@ -1011,6 +1206,7 @@ def iot_edge_deployment_create( resource_group_name=resource_group_name, login=login, config_type=config_type, + auth_type_dataplane=auth_type_dataplane, ) @@ -1025,6 +1221,7 @@ def iot_hub_configuration_create( metrics=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): return _iot_hub_configuration_create( cmd=cmd, @@ -1038,6 +1235,7 @@ def iot_hub_configuration_create( resource_group_name=resource_group_name, login=login, config_type=ConfigType.adm, + auth_type_dataplane=auth_type_dataplane, ) @@ -1053,6 +1251,7 @@ def _iot_hub_configuration_create( metrics=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): from azext_iot.sdk.iothub.service.models import ( Configuration, @@ -1062,7 +1261,10 @@ def _iot_hub_configuration_create( discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1203,14 +1405,24 @@ def _validate_payload_schema(content): def iot_hub_configuration_update( - cmd, config_id, parameters, hub_name=None, resource_group_name=None, login=None, etag=None + cmd, + config_id, + parameters, + hub_name=None, + resource_group_name=None, + login=None, + etag=None, + auth_type_dataplane=None, ): from azext_iot.sdk.iothub.service.models import Configuration from azext_iot.common.utility import verify_transform discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1229,7 +1441,7 @@ def iot_hub_configuration_update( content=parameters["content"], metrics=parameters.get("metrics", None), target_condition=parameters["targetCondition"], - priority=parameters["priority"] + priority=parameters["priority"], ) return service_sdk.configuration.create_or_update( id=config_id, configuration=config, custom_headers=headers @@ -1241,11 +1453,19 @@ def iot_hub_configuration_update( def iot_hub_configuration_show( - cmd, config_id, hub_name=None, resource_group_name=None, login=None + cmd, + config_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) return _iot_hub_configuration_show(target=target, config_id=config_id) @@ -1261,13 +1481,19 @@ def _iot_hub_configuration_show(target, config_id): def iot_hub_configuration_list( - cmd, hub_name=None, top=None, resource_group_name=None, login=None + cmd, + hub_name=None, + top=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): result = _iot_hub_configuration_list( - cmd, + cmd=cmd, hub_name=hub_name, resource_group_name=resource_group_name, login=login, + auth_type_dataplane=auth_type_dataplane, ) filtered = [ c @@ -1281,13 +1507,19 @@ def iot_hub_configuration_list( def iot_edge_deployment_list( - cmd, hub_name=None, top=None, resource_group_name=None, login=None + cmd, + hub_name=None, + top=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): result = _iot_hub_configuration_list( cmd, hub_name=hub_name, resource_group_name=resource_group_name, login=login, + auth_type_dataplane=auth_type_dataplane, ) filtered = [c for c in result if c["content"].get("modulesContent") is not None] @@ -1295,11 +1527,14 @@ def iot_edge_deployment_list( def _iot_hub_configuration_list( - cmd, hub_name=None, resource_group_name=None, login=None + cmd, hub_name=None, resource_group_name=None, login=None, auth_type_dataplane=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1314,11 +1549,20 @@ def _iot_hub_configuration_list( def iot_hub_configuration_delete( - cmd, config_id, hub_name=None, resource_group_name=None, login=None, etag=None + cmd, + config_id, + hub_name=None, + resource_group_name=None, + login=None, + etag=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1339,6 +1583,7 @@ def iot_edge_deployment_metric_show( hub_name=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): return iot_hub_configuration_metric_show( cmd, @@ -1348,6 +1593,7 @@ def iot_edge_deployment_metric_show( hub_name=hub_name, resource_group_name=resource_group_name, login=login, + auth_type_dataplane=auth_type_dataplane, ) @@ -1359,10 +1605,14 @@ def iot_hub_configuration_metric_show( hub_name=None, resource_group_name=None, login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1404,11 +1654,19 @@ def iot_hub_configuration_metric_show( def iot_device_twin_show( - cmd, device_id, hub_name=None, resource_group_name=None, login=None + cmd, + device_id, + hub_name=None, + resource_group_name=None, + login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) return _iot_device_twin_show(target=target, device_id=device_id) @@ -1438,13 +1696,23 @@ def iot_twin_update_custom(instance, desired=None, tags=None): def iot_device_twin_update( - cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None, etag=None + cmd, + device_id, + parameters, + hub_name=None, + resource_group_name=None, + login=None, + etag=None, + auth_type_dataplane=None, ): from azext_iot.common.utility import verify_transform discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1469,11 +1737,21 @@ def iot_device_twin_update( def iot_device_twin_replace( - cmd, device_id, target_json, hub_name=None, resource_group_name=None, login=None, etag=None + cmd, + device_id, + target_json, + hub_name=None, + resource_group_name=None, + login=None, + etag=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1498,6 +1776,7 @@ def iot_device_method( timeout=30, resource_group_name=None, login=None, + auth_type_dataplane=None, ): from azext_iot.constants import ( METHOD_INVOKE_MAX_TIMEOUT_SEC, @@ -1515,7 +1794,10 @@ def iot_device_method( discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1536,7 +1818,9 @@ def iot_device_method( } return service_sdk.devices.invoke_method( - device_id=device_id, direct_method_request=request_body, timeout=timeout, + device_id=device_id, + direct_method_request=request_body, + timeout=timeout, ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -1555,6 +1839,7 @@ def iot_device_module_method( timeout=30, resource_group_name=None, login=None, + auth_type_dataplane=None, ): from azext_iot.constants import ( METHOD_INVOKE_MAX_TIMEOUT_SEC, @@ -1572,7 +1857,10 @@ def iot_device_module_method( discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1615,6 +1903,7 @@ def iot_get_sas_token( resource_group_name=None, login=None, module_id=None, + auth_type_dataplane=None, ): key_type = key_type.lower() policy_name = policy_name.lower() @@ -1633,7 +1922,7 @@ def iot_get_sas_token( ) return { - "sas": _iot_build_sas_token( + DeviceAuthApiType.sas.value: _iot_build_sas_token( cmd, hub_name, device_id, @@ -1643,6 +1932,7 @@ def iot_get_sas_token( duration, resource_group_name, login, + auth_type_dataplane, ).generate_sas_token() } @@ -1657,18 +1947,24 @@ def _iot_build_sas_token( duration=3600, resource_group_name=None, login=None, + auth_type_dataplane=None, ): from azext_iot.common._azure import ( parse_iot_device_connection_string, parse_iot_device_module_connection_string, ) + # There is no dataplane operation for a pure IoT Hub sas token + if all([device_id is None, module_id is None]): + auth_type_dataplane = "key" + discovery = IotHubDiscovery(cmd) target = discovery.get_target( hub_name=hub_name, resource_group_name=resource_group_name, policy_name=policy_name, login=login, + auth_type=auth_type_dataplane, ) uri = None policy = None @@ -1724,13 +2020,13 @@ def _build_device_or_module_connection_string(entity, key_type="primary"): ) auth = entity["authentication"] auth_type = auth["type"].lower() - if auth_type == "sas": + if auth_type == DeviceAuthApiType.sas.value.lower(): key = "SharedAccessKey={}".format( auth["symmetricKey"]["primaryKey"] if key_type == "primary" else auth["symmetricKey"]["secondaryKey"] ) - elif auth_type in ["certificateauthority", "selfsigned"]: + elif auth_type in [DeviceAuthApiType.certificateAuthority.value.lower(), DeviceAuthApiType.selfSigned.value.lower()]: key = "x509=true" else: raise CLIError("Unable to form target connection string") @@ -1750,6 +2046,7 @@ def iot_get_device_connection_string( key_type="primary", resource_group_name=None, login=None, + auth_type_dataplane=None, ): result = {} device = iot_device_show( @@ -1758,6 +2055,7 @@ def iot_get_device_connection_string( hub_name=hub_name, resource_group_name=resource_group_name, login=login, + auth_type_dataplane=auth_type_dataplane, ) result["connectionString"] = _build_device_or_module_connection_string( device, key_type @@ -1773,6 +2071,7 @@ def iot_get_module_connection_string( key_type="primary", resource_group_name=None, login=None, + auth_type_dataplane=None, ): result = {} module = iot_device_module_show( @@ -1782,6 +2081,7 @@ def iot_get_module_connection_string( resource_group_name=resource_group_name, hub_name=hub_name, login=login, + auth_type_dataplane=auth_type_dataplane, ) result["connectionString"] = _build_device_or_module_connection_string( module, key_type @@ -1972,18 +2272,24 @@ def _iot_c2d_message_receive(target, device_id, lock_timeout=60, ack=None): ack_response = {} if ack == SettleType.abandon.value: logger.debug("__Abandoning message__") - ack_response = device_sdk.device.abandon_device_bound_notification( - id=device_id, etag=eTag, raw=True + ack_response = ( + device_sdk.device.abandon_device_bound_notification( + id=device_id, etag=eTag, raw=True + ) ) elif ack == SettleType.reject.value: logger.debug("__Rejecting message__") - ack_response = device_sdk.device.complete_device_bound_notification( - id=device_id, etag=eTag, reject="", raw=True + ack_response = ( + device_sdk.device.complete_device_bound_notification( + id=device_id, etag=eTag, reject="", raw=True + ) ) else: logger.debug("__Completing message__") - ack_response = device_sdk.device.complete_device_bound_notification( - id=device_id, etag=eTag, raw=True + ack_response = ( + device_sdk.device.complete_device_bound_notification( + id=device_id, etag=eTag, raw=True + ) ) payload["ack"] = ( @@ -2017,7 +2323,7 @@ def _iot_c2d_message_receive(target, device_id, lock_timeout=60, ack=None): if result.content: target_encoding = result.headers.get("ContentEncoding", "utf-8") logger.info(f"Decoding message data encoded with: {target_encoding}") - payload["data"] = result.content.decode(encoding=target_encoding) + payload["data"] = result.content.decode(target_encoding) return payload return @@ -2043,6 +2349,7 @@ def iot_c2d_message_send( repair=False, resource_group_name=None, login=None, + auth_type_dataplane=None, ): from azext_iot.common.deps import ensure_uamqp from azext_iot.common.utility import validate_min_python_version @@ -2059,7 +2366,10 @@ def iot_c2d_message_send( discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) if properties: @@ -2179,6 +2489,10 @@ def http_wrap(target, device_id, generator, msg_interval, msg_count): if protocol_type == ProtocolType.mqtt.name: device = _iot_device_show(target, device_id) device_connection_string = _build_device_or_module_connection_string(device, KeyType.primary.value) + + if device and device.get("authentication", {}).get("type", "") != DeviceAuthApiType.sas.value: + raise CLIError('MQTT simulation is only supported for symmetric key auth (SAS) based devices') + client_mqtt = mqtt_client( target=target, device_conn_string=device_connection_string, @@ -2205,16 +2519,24 @@ def http_wrap(target, device_id, generator, msg_interval, msg_count): def iot_c2d_message_purge( - cmd, device_id, hub_name=None, resource_group_name=None, login=None, + cmd, + device_id, + hub_name=None, + resource_group_name=None, + login=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) - return service_sdk.cloud_to_device_messages.purge_cloud_to_device_message_queue(device_id) + return service_sdk.cloud_to_device_messages.purge_cloud_to_device_message_queue( + device_id + ) def _iot_simulate_get_default_properties(protocol): @@ -2252,10 +2574,10 @@ def iot_device_export( blob_container_uri, include_keys=False, storage_authentication_type=None, + identity=None, resource_group_name=None, ): from azext_iot._factory import iot_hub_service_factory - client = iot_hub_service_factory(cmd.cli_ctx) discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -2274,13 +2596,34 @@ def iot_device_export( if storage_authentication_type else None ) + export_request = ExportDevicesRequest( export_blob_container_uri=blob_container_uri, exclude_keys=not include_keys, authentication_type=storage_authentication_type, ) + + user_identity = identity not in [None, '[system]'] + if user_identity and storage_authentication_type != AuthenticationType.identityBased.name: + raise CLIError( + "Device export with user-assigned identities requires identity-based authentication [--storage-auth-type]" + ) + # Track 2 CLI SDKs provide support for user-assigned identity objects + if ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION) and user_identity: + from azure.mgmt.iothub.models import ManagedIdentity # pylint: disable=no-name-in-module + export_request.identity = ManagedIdentity(user_assigned_identity=identity) + + # if the user supplied a user-assigned identity, let them know they need a new CLI/SDK + elif user_identity: + raise CLIError( + "Device export with user-assigned identities requires a dependency of azure-mgmt-iothub>={}" + .format(IOTHUB_TRACK_2_SDK_MIN_VERSION) + ) + return client.export_devices( - target["resourcegroup"], hub_name, export_devices_parameters=export_request, + target["resourcegroup"], + hub_name, + export_devices_parameters=export_request, ) if storage_authentication_type: raise CLIError( @@ -2301,6 +2644,7 @@ def iot_device_import( output_blob_container_uri, storage_authentication_type=None, resource_group_name=None, + identity=None, ): from azext_iot._factory import iot_hub_service_factory @@ -2318,6 +2662,7 @@ def iot_device_import( if ensure_iothub_sdk_min_version("0.12.0"): from azure.mgmt.iothub.models import ImportDevicesRequest + from azext_iot.common.shared import AuthenticationType storage_authentication_type = ( @@ -2325,6 +2670,7 @@ def iot_device_import( if storage_authentication_type else None ) + import_request = ImportDevicesRequest( input_blob_container_uri=input_blob_container_uri, output_blob_container_uri=output_blob_container_uri, @@ -2332,8 +2678,27 @@ def iot_device_import( output_blob_name=None, authentication_type=storage_authentication_type, ) + + user_identity = identity not in [None, '[system]'] + if user_identity and storage_authentication_type != AuthenticationType.identityBased.name: + raise CLIError( + "Device import with user-assigned identities requires identity-based authentication [--storage-auth-type]" + ) + # Track 2 CLI SDKs provide support for user-assigned identity objects + if ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION) and user_identity: + from azure.mgmt.iothub.models import ManagedIdentity # pylint: disable=no-name-in-module + import_request.identity = ManagedIdentity(user_assigned_identity=identity) + # if the user supplied a user-assigned identity, let them know they need a new CLI/SDK + elif user_identity: + raise CLIError( + "Device import with user-assigned identities requires a dependency of azure-mgmt-iothub>={}" + .format(IOTHUB_TRACK_2_SDK_MIN_VERSION) + ) + return client.import_devices( - target["resourcegroup"], hub_name, import_devices_parameters=import_request, + target["resourcegroup"], + hub_name, + import_devices_parameters=import_request, ) if storage_authentication_type: raise CLIError( @@ -2448,6 +2813,7 @@ def iot_hub_monitor_feedback( repair=False, resource_group_name=None, login=None, + auth_type_dataplane=None, ): from azext_iot.common.deps import ensure_uamqp from azext_iot.common.utility import validate_min_python_version @@ -2459,7 +2825,10 @@ def iot_hub_monitor_feedback( discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + auth_type=auth_type_dataplane, ) return _iot_hub_monitor_feedback( @@ -2468,11 +2837,17 @@ def iot_hub_monitor_feedback( def iot_hub_distributed_tracing_show( - cmd, hub_name, device_id, resource_group_name=None, login=None, + cmd, + hub_name, + device_id, + resource_group_name=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, resource_group_name=resource_group_name, login=login + hub_name=hub_name, + resource_group_name=resource_group_name, + auth_type=auth_type_dataplane, ) device_twin = _iot_hub_distributed_tracing_show(target=target, device_id=device_id) @@ -2565,14 +2940,14 @@ def iot_hub_distributed_tracing_update( sampling_mode, sampling_rate, resource_group_name=None, - login=None, + auth_type_dataplane=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( hub_name=hub_name, resource_group_name=resource_group_name, include_events=True, - login=login, + auth_type=auth_type_dataplane, ) if int(sampling_rate) not in range(0, 101): @@ -2589,7 +2964,7 @@ def iot_hub_distributed_tracing_update( 1 if sampling_mode.lower() == "on" else 2 ) result = iot_device_twin_update( - cmd, device_id, device_twin, hub_name, resource_group_name, login + cmd, device_id, device_twin, hub_name, resource_group_name ) return _customize_device_tracing_output( result.device_id, result.properties.desired, result.properties.reported @@ -2621,20 +2996,26 @@ def conn_str_getter(hub): for hub in hubs: if hub.properties.state == IoTHubStateType.Active.value: try: - connection_strings.append({ - "name": hub.name, - "connectionString": conn_str_getter(hub) - if show_all - else conn_str_getter(hub)[0], - }) + connection_strings.append( + { + "name": hub.name, + "connectionString": conn_str_getter(hub) + if show_all + else conn_str_getter(hub)[0], + } + ) except: - logger.warning(f"Warning: The IoT Hub {hub.name} in resource group " + - f"{hub.additional_properties['resourcegroup']} does " + - f"not have the target policy {policy_name}.") + logger.warning( + f"Warning: The IoT Hub {hub.name} in resource group " + + f"{hub.additional_properties['resourcegroup']} does " + + f"not have the target policy {policy_name}." + ) else: - logger.warning(f"Warning: The IoT Hub {hub.name} in resource group " + - f"{hub.additional_properties['resourcegroup']} is skipped " + - "because the hub is not active.") + logger.warning( + f"Warning: The IoT Hub {hub.name} in resource group " + + f"{hub.additional_properties['resourcegroup']} is skipped " + + "because the hub is not active." + ) return connection_strings hub = discovery.find_iothub(hub_name, resource_group_name) @@ -2660,7 +3041,6 @@ def _get_hub_connection_string( hub.name, hub.additional_properties["resourcegroup"], policy_name ) ) - if default_eventhub: cs_template_eventhub = ( "Endpoint={};SharedAccessKeyName={};SharedAccessKey={};EntityPath={}" @@ -2677,7 +3057,12 @@ def _get_hub_connection_string( entityPath, ) for p in policies - if "serviceconnect" in (p.rights.value.lower() if isinstance(p.rights, (Enum, EnumMeta)) else p.rights.lower()) + if "serviceconnect" + in ( + p.rights.value.lower() + if isinstance(p.rights, (Enum, EnumMeta)) + else p.rights.lower() + ) ] hostname = hub.properties.host_name diff --git a/azext_iot/tests/__init__.py b/azext_iot/tests/__init__.py index f7cf926d2..00e227551 100644 --- a/azext_iot/tests/__init__.py +++ b/azext_iot/tests/__init__.py @@ -8,8 +8,10 @@ import io import os +from azext_iot.tests.iothub import DATAPLANE_AUTH_TYPES from azure.cli.testsdk import LiveScenarioTest from contextlib import contextmanager +from typing import List PREFIX_DEVICE = "test-device-" PREFIX_EDGE_DEVICE = "test-edge-device-" @@ -76,15 +78,46 @@ def __init__(self, test_scenario, entity_name, entity_rg): self.entity_name = entity_name self.entity_rg = entity_rg - self.device_ids = [] - self.config_ids = [] - - os.environ["AZURE_CORE_COLLECT_TELEMETRY"] = "no" super(IoTLiveScenarioTest, self).__init__(test_scenario) self.region = self.get_region() self.connection_string = self.get_hub_cstring() + def clean_up(self, device_ids: List[str] = None, config_ids: List[str] = None): + if device_ids: + device = device_ids.pop() + self.cmd( + "iot hub device-identity delete -d {} --login {}".format( + device, self.connection_string + ), + checks=self.is_empty(), + ) + + for device in device_ids: + self.cmd( + "iot hub device-identity delete -d {} -n {} -g {}".format( + device, self.entity_name, self.entity_rg + ), + checks=self.is_empty(), + ) + + if config_ids: + config = config_ids.pop() + self.cmd( + "iot hub configuration delete -c {} --login {}".format( + config, self.connection_string + ), + checks=self.is_empty(), + ) + + for config in config_ids: + self.cmd( + "iot hub configuration delete -c {} -n {} -g {}".format( + config, self.entity_name, self.entity_rg + ), + checks=self.is_empty(), + ) + def generate_device_names(self, count=1, edge=False): names = [ self.create_random_name( @@ -92,7 +125,6 @@ def generate_device_names(self, count=1, edge=False): ) for i in range(count) ] - self.device_ids.extend(names) return names def generate_module_names(self, count=1): @@ -108,7 +140,6 @@ def generate_config_names(self, count=1, edge=False): ) for i in range(count) ] - self.config_ids.extend(names) return names def generate_job_names(self, count=1): @@ -117,39 +148,21 @@ def generate_job_names(self, count=1): ] def tearDown(self): - if self.device_ids: - device = self.device_ids.pop() - self.cmd( - "iot hub device-identity delete -d {} --login {}".format( - device, self.connection_string - ), - checks=self.is_empty(), - ) + device_list = [] + device_list.extend(d["deviceId"] for d in self.cmd( + f"iot hub device-identity list -n {self.entity_name} -g {self.entity_rg}" + ).get_output_in_json()) - for device in self.device_ids: - self.cmd( - "iot hub device-identity delete -d {} -n {} -g {}".format( - device, self.entity_name, self.entity_rg - ), - checks=self.is_empty(), - ) + config_list = [] + config_list.extend(c["id"] for c in self.cmd( + f"iot edge deployment list -n {self.entity_name} -g {self.entity_rg}" + ).get_output_in_json()) - if self.config_ids: - config = self.config_ids.pop() - self.cmd( - "iot hub configuration delete -c {} --login {}".format( - config, self.connection_string - ), - checks=self.is_empty(), - ) + config_list.extend(c["id"] for c in self.cmd( + f"iot hub configuration list -n {self.entity_name} -g {self.entity_rg}" + ).get_output_in_json()) - for config in self.config_ids: - self.cmd( - "iot hub configuration delete -c {} -n {} -g {}".format( - config, self.entity_name, self.entity_rg - ), - checks=self.is_empty(), - ) + self.clean_up(device_ids=device_list, config_ids=config_list) def get_region(self): result = self.cmd( @@ -162,11 +175,21 @@ def get_region(self): def get_hub_cstring(self, policy="iothubowner"): return self.cmd( - "iot hub show-connection-string -n {} -g {} --policy-name {}".format( + "iot hub connection-string show -n {} -g {} --policy-name {}".format( self.entity_name, self.entity_rg, policy ) ).get_output_in_json()["connectionString"] + def set_cmd_auth_type(self, command: str, auth_type: str) -> str: + if auth_type not in DATAPLANE_AUTH_TYPES: + raise RuntimeError(f"auth_type of: {auth_type} is unsupported.") + + # cstring takes precedence + if auth_type == "cstring": + return f"{command} --login {self.connection_string}" + + return f"{command} --auth-type {auth_type}" + def disable_telemetry(test_function): def wrapper(*args, **kwargs): diff --git a/azext_iot/tests/central/json/deeply_nested_template.json b/azext_iot/tests/central/json/deeply_nested_template.json index 7231d657d..03bdd9061 100644 Binary files a/azext_iot/tests/central/json/deeply_nested_template.json and b/azext_iot/tests/central/json/deeply_nested_template.json differ diff --git a/azext_iot/tests/central/json/device_template.json b/azext_iot/tests/central/json/device_template.json index 41cedaf66..61c580948 100644 --- a/azext_iot/tests/central/json/device_template.json +++ b/azext_iot/tests/central/json/device_template.json @@ -1,285 +1,204 @@ { - "id": "urn:d9cltbeus:tvj4oal1a0", - "etag": "\"~WgqHZmg+d95gTA53P8AnqBsDLGgj2wa0msOL7xozC9Y=\"", - "types": [ - "DeviceModel" - ], + "etag": "\"~Wh8etPOe6WmVnj+WRh7k721V17PaQqCFzgzhNhjyzI4=\"", "displayName": "duplicate-field-name", "capabilityModel": { - "@id": "urn:sampleApp:modelOne_bz:2", - "@type": "CapabilityModel", - "implements": [ + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_bz:2", + "@type": "Interface", + "contents": [], + "displayName": "larger-telemetry-device", + "extends": [ { - "@id": "urn:sampleApp:modelOne_bz:_rpgcmdpo:1", - "@type": [ - "InterfaceInstance" - ], - "displayName": "Interface", - "name": "modelOne_g4", - "schema": { - "@id": "urn:sampleApp:modelOne_g4:1", - "@type": [ - "Interface" - ], - "displayName": "Interface", - "contents": [ - { - "@id": "urn:sampleApp:modelOne_g4:Bool:1", - "@type": [ - "Telemetry" - ], - "displayName": "Bool", - "name": "Bool", - "schema": "boolean" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Date:1", - "@type": [ - "Telemetry" - ], - "displayName": "Date", - "name": "Date", - "schema": "date" - }, - { - "@id": "urn:sampleApp:modelOne_g4:DateTime:1", - "@type": [ - "Telemetry" - ], - "displayName": "DateTime", - "name": "DateTime", - "schema": "dateTime" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Double:1", - "@type": [ - "Telemetry" - ], - "displayName": "Double", - "name": "Double", - "schema": "double" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Duration:1", - "@type": [ - "Telemetry" - ], - "displayName": "Duration", - "name": "Duration", - "schema": "duration" - }, - { - "@id": "urn:sampleApp:modelOne_g4:IntEnum:1", - "@type": [ - "Telemetry" - ], - "displayName": "IntEnum", - "name": "IntEnum", - "schema": { - "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:1", - "@type": [ - "Enum" - ], - "displayName": "Enum", - "valueSchema": "integer", - "enumValues": [ - { - "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum1:1", - "@type": [ - "EnumValue" - ], - "displayName": "Enum1", - "enumValue": 1, - "name": "Enum1" - }, - { - "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum2:1", - "@type": [ - "EnumValue" - ], - "displayName": "Enum2", - "enumValue": 2, - "name": "Enum2" - } - ] - } - }, - { - "@id": "urn:sampleApp:modelOne_g4:StringEnum:1", - "@type": [ - "Telemetry" - ], - "displayName": "StringEnum", - "name": "StringEnum", - "schema": { - "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:1", - "@type": [ - "Enum" - ], - "displayName": "Enum", - "valueSchema": "string", - "enumValues": [ - { - "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumA:1", - "@type": [ - "EnumValue" - ], - "displayName": "EnumA", - "enumValue": "A", - "name": "EnumA" - }, - { - "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumB:1", - "@type": [ - "EnumValue" - ], - "displayName": "EnumB", - "enumValue": "B", - "name": "EnumB" - } - ] - } - }, - { - "@id": "urn:sampleApp:modelOne_g4:Float:1", - "@type": [ - "Telemetry" - ], - "displayName": "Float", - "name": "Float", - "schema": "float" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Geopoint:1", - "@type": [ - "Telemetry" - ], - "displayName": "Geopoint", - "name": "Geopoint", - "schema": "geopoint" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Int:1", - "@type": [ - "Telemetry" - ], - "displayName": "Int", - "name": "Int", - "schema": "integer" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Long:1", - "@type": [ - "Telemetry" - ], - "displayName": "Long", - "name": "Long", - "schema": "long" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Object:1", - "@type": [ - "Telemetry" - ], + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:1", + "@type": "Interface", + "contents": [ + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Bool:1", + "@type": "Telemetry", + "displayName": "Bool", + "name": "Bool", + "schema": "boolean" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Date:1", + "@type": "Telemetry", + "displayName": "Date", + "name": "Date", + "schema": "date" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:DateTime:1", + "@type": "Telemetry", + "displayName": "DateTime", + "name": "DateTime", + "schema": "dateTime" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Double:1", + "@type": "Telemetry", + "displayName": "Double", + "name": "Double", + "schema": "double" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Duration:1", + "@type": "Telemetry", + "displayName": "Duration", + "name": "Duration", + "schema": "duration" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:IntEnum:1", + "@type": "Telemetry", + "displayName": "IntEnum", + "name": "IntEnum", + "schema": { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:IntEnum:pgkbdhard:1", + "@type": "Enum", + "displayName": "Enum", + "enumValues": [ + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:IntEnum:pgkbdhard:Enum1:1", + "displayName": "Enum1", + "enumValue": 1, + "name": "Enum1" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:IntEnum:pgkbdhard:Enum2:1", + "displayName": "Enum2", + "enumValue": 2, + "name": "Enum2" + } + ], + "valueSchema": "integer" + } + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:StringEnum:1", + "@type": "Telemetry", + "displayName": "StringEnum", + "name": "StringEnum", + "schema": { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:StringEnum:kyesuinpsx:1", + "@type": "Enum", + "displayName": "Enum", + "enumValues": [ + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:StringEnum:kyesuinpsx:EnumA:1", + "displayName": "EnumA", + "enumValue": "A", + "name": "EnumA" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:StringEnum:kyesuinpsx:EnumB:1", + "displayName": "EnumB", + "enumValue": "B", + "name": "EnumB" + } + ], + "valueSchema": "string" + } + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Float:1", + "@type": "Telemetry", + "displayName": "Float", + "name": "Float", + "schema": "float" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Geopoint:1", + "@type": "Telemetry", + "displayName": "Geopoint", + "name": "Geopoint", + "schema": "geopoint" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Int:1", + "@type": "Telemetry", + "displayName": "Int", + "name": "Int", + "schema": "integer" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Long:1", + "@type": "Telemetry", + "displayName": "Long", + "name": "Long", + "schema": "long" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Object:1", + "@type": "Telemetry", + "displayName": "Object", + "name": "Object", + "schema": { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Object:8ot2x5whp8:1", + "@type": "Object", "displayName": "Object", - "name": "Object", - "schema": { - "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:1", - "@type": [ - "Object" - ], - "displayName": "Object", - "fields": [ - { - "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:Double:1", - "@type": [ - "SchemaField" - ], - "displayName": "Double", - "name": "Double", - "schema": "double" - } - ] - } - }, - { - "@id": "urn:sampleApp:modelOne_g4:String:1", - "@type": [ - "Telemetry" - ], - "displayName": "String", - "name": "String", - "schema": "string" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Time:1", - "@type": [ - "Telemetry" - ], - "displayName": "Time", - "name": "Time", - "schema": "time" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Vector:1", - "@type": [ - "Telemetry" - ], - "displayName": "Vector", - "name": "Vector", - "schema": "vector" + "fields": [ + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Object:8ot2x5whp8:Double:1", + "displayName": "Double", + "name": "Double", + "schema": "double" + } + ] } - ] - } + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:String:1", + "@type": "Telemetry", + "displayName": "String", + "name": "String", + "schema": "string" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Time:1", + "@type": "Telemetry", + "displayName": "Time", + "name": "Time", + "schema": "time" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:largerTelemetryDevice_g4:Vector:1", + "@type": "Telemetry", + "displayName": "Vector", + "name": "Vector", + "schema": "vector" + } + ], + "displayName": "Interface" }, { - "@id": "urn:sampleApp:modelOne_bz:myxqftpsr:2", - "@type": [ - "InterfaceInstance" + "@id": "urn:azCliDevelopmentFlashmagnus:duplicateFieldName_ed:1", + "@type": "Interface", + "contents": [ + { + "@id": "urn:azCliDevelopmentFlashmagnus:duplicateFieldName_ed:Bool:1", + "@type": "Telemetry", + "displayName": "Bool", + "name": "Bool", + "schema": "boolean" + }, + { + "@id": "urn:azCliDevelopmentFlashmagnus:duplicateFieldName_ed:bool:1", + "@type": "Telemetry", + "displayName": "bool", + "name": "bool", + "schema": "boolean" + } ], - "displayName": "Interface", - "name": "modelTwo_ed", - "schema": { - "@id": "urn:sampleApp:modelTwo_ed:1", - "@type": [ - "Interface" - ], - "displayName": "Interface", - "contents": [ - { - "@id": "urn:sampleApp:modelTwo_ed:Bool:1", - "@type": [ - "Telemetry" - ], - "displayName": "Bool", - "name": "Bool", - "schema": "boolean" - }, - { - "@id": "urn:sampleApp:modelTwo_ed:bool:1", - "@type": [ - "Telemetry" - ], - "displayName": "bool", - "name": "bool", - "schema": "boolean" - } - ] - } + "displayName": "Interface" } - ], - "displayName": "larger-telemetry-device", - "@context": [ - "http://azureiot.com/v1/contexts/IoTModel.json" ] }, - "solutionModel": { - "@id": "urn:d9cltbeus:lz1tl4a_jz", - "@type": [ - "SolutionModel" - ], - "cloudProperties": [], - "initialValues": [], - "overrides": [] - } + "@id": "urn:d9cltbeus:tvj4oal1a0", + "@type": [ + "ModelDefinition", + "DeviceModel" + ], + "@context": [ + "dtmi:iotcentral:context;2", + "dtmi:dtdl:context;2" + ] } \ No newline at end of file diff --git a/azext_iot/tests/central/json/device_template_int_test.json b/azext_iot/tests/central/json/device_template_int_test.json index 689656bf8..1e238e4ba 100644 --- a/azext_iot/tests/central/json/device_template_int_test.json +++ b/azext_iot/tests/central/json/device_template_int_test.json @@ -1,320 +1,211 @@ { - "types": [ - "DeviceModel" - ], - "displayName": "int-test-device-template", + "displayName": "dtmi:intTestDeviceTemplate", "capabilityModel": { - "@id": "urn:sampleApp:modelOne_bz:2", - "@type": "CapabilityModel", + "@id": "dtmi:sampleApp:modelOnebz;3", + "@type": "Interface", "contents": [ { - "@id": "urn:testazuresphere:AzureSphereSampleDevice_614:testDefaultCapability:2", + "@id": "dtmi:testazuresphere:AzureSphereSampleDevice614:testDefaultCapability;2", "@type": "Telemetry", "displayName": "testDefaultCapability", "name": "testDefaultCapability", "schema": "double" - } - ], - "implements": [ + }, { - "@id": "urn:sampleApp:modelOne_bz:_rpgcmdpo:1", - "@type": [ - "InterfaceInstance" - ], - "displayName": "Interface", - "name": "modelOne_g4", + "@id": "dtmi:sampleApp:modelOnebz:dtmiIntTestDeviceTemplateV33jl;3", + "@type": "Component", + "displayName": "Component", + "name": "dtmiIntTestDeviceTemplateV33jl", "schema": { - "@id": "urn:sampleApp:modelOne_g4:1", - "@type": [ - "Interface" - ], - "displayName": "Interface", + "@id": "dtmi:cliIntegrationtestApp:dtmiIntTestDeviceTemplateV33jl;1", + "@type": "Interface", "contents": [ { - "@id": "urn:sampleApp:modelOne_g4:Bool:1", - "@type": [ - "Telemetry" - ], - "displayName": "Bool", - "name": "Bool", - "schema": "boolean" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Date:1", - "@type": [ - "Telemetry" - ], - "displayName": "Date", - "name": "Date", - "schema": "date" - }, - { - "@id": "urn:sampleApp:modelOne_g4:DateTime:1", - "@type": [ - "Telemetry" - ], - "displayName": "DateTime", - "name": "DateTime", - "schema": "dateTime" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Double:1", - "@type": [ - "Telemetry" - ], - "displayName": "Double", - "name": "Double", - "schema": "double" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Duration:1", - "@type": [ - "Telemetry" - ], - "displayName": "Duration", - "name": "Duration", - "schema": "duration" - }, - { - "@id": "urn:sampleApp:modelOne_g4:IntEnum:1", - "@type": [ - "Telemetry" - ], - "displayName": "IntEnum", - "name": "IntEnum", - "schema": { - "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:1", - "@type": [ - "Enum" - ], - "displayName": "Enum", - "valueSchema": "integer", - "enumValues": [ - { - "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum1:1", - "@type": [ - "EnumValue" - ], - "displayName": "Enum1", - "enumValue": 1, - "name": "Enum1" - }, - { - "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum2:1", - "@type": [ - "EnumValue" - ], - "displayName": "Enum2", - "enumValue": 2, - "name": "Enum2" - } - ] - } - }, - { - "@id": "urn:sampleApp:modelOne_g4:StringEnum:1", - "@type": [ - "Telemetry" - ], - "displayName": "StringEnum", - "name": "StringEnum", - "schema": { - "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:1", - "@type": [ - "Enum" - ], - "displayName": "Enum", - "valueSchema": "string", - "enumValues": [ - { - "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumA:1", - "@type": [ - "EnumValue" - ], - "displayName": "EnumA", - "enumValue": "A", - "name": "EnumA" - }, - { - "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumB:1", - "@type": [ - "EnumValue" - ], - "displayName": "EnumB", - "enumValue": "B", - "name": "EnumB" - } - ] - } - }, - { - "@id": "urn:sampleApp:modelOne_g4:Float:1", - "@type": [ - "Telemetry" - ], - "displayName": "Float", - "name": "Float", - "schema": "float" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Geopoint:1", - "@type": [ - "Telemetry" - ], - "displayName": "Geopoint", - "name": "Geopoint", - "schema": "geopoint" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Int:1", - "@type": [ - "Telemetry" - ], - "displayName": "Int", - "name": "Int", - "schema": "integer" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Long:1", - "@type": [ - "Telemetry" - ], - "displayName": "Long", - "name": "Long", - "schema": "long" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Object:1", - "@type": [ - "Telemetry" - ], - "displayName": "Object", - "name": "Object", - "schema": { - "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:1", - "@type": [ - "Object" - ], - "displayName": "Object", - "fields": [ - { - "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:Double:1", - "@type": [ - "SchemaField" - ], - "displayName": "Double", - "name": "Double", - "schema": "double" - } - ] - } - }, - { - "@id": "urn:sampleApp:modelOne_g4:String:1", - "@type": [ - "Telemetry" - ], - "displayName": "String", - "name": "String", - "schema": "string" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Time:1", - "@type": [ - "Telemetry" - ], - "displayName": "Time", - "name": "Time", - "schema": "time" - }, - { - "@id": "urn:sampleApp:modelOne_g4:Vector:1", - "@type": [ - "Telemetry" - ], - "displayName": "Vector", - "name": "Vector", - "schema": "vector" - }, - { - "@id": "urn:sampleApp:modelOne_g4:sync_cmd:1", - "@type": [ - "Command" - ], + "@id": "dtmi:cliIntegrationtestApp:dtmiIntTestDeviceTemplateV33jl:testCommand;1", + "@type": "Command", "commandType": "synchronous", - "displayName": "sync_cmd", - "durable": false, - "name": "sync_cmd", - "request": { - "@id": "urn:sampleApp:modelOne_g4:sync_cmd:argument:1", - "@type": [ - "SchemaField" - ], - "displayName": "argument", - "name": "argument", - "schema": "string" - }, - "response": { - "@id": "urn:sampleApp:modelOne_g4:sync_cmd:status:1", - "@type": [ - "SchemaField" - ], - "displayName": "status", - "name": "status", - "schema": "double" - } + "displayName": "testCommand", + "name": "testCommand" } - ] - } - }, - { - "@id": "urn:sampleApp:modelOne_bz:myxqftpsr:2", - "@type": [ - "InterfaceInstance" - ], - "displayName": "Interface", - "name": "modelTwo_ed", - "schema": { - "@id": "urn:sampleApp:modelTwo_ed:1", - "@type": [ - "Interface" ], - "displayName": "Interface", - "contents": [ - { - "@id": "urn:sampleApp:modelTwo_ed:Bool:1", - "@type": [ - "Telemetry" - ], - "displayName": "Bool", - "name": "Bool", - "schema": "boolean" - }, - { - "@id": "urn:sampleApp:modelTwo_ed:bool:1", - "@type": [ - "Telemetry" - ], - "displayName": "bool", - "name": "bool", - "schema": "boolean" - } - ] + "displayName": "Component" } } ], "displayName": "larger-telemetry-device", - "@context": [ - "http://azureiot.com/v1/contexts/IoTModel.json" + "extends": [ + { + "@id": "dtmi:sampleApp:modelOneg4;1", + "@type": "Interface", + "contents": [ + { + "@id": "dtmi:sampleApp:modelOneg4:Bool;1", + "@type": "Telemetry", + "displayName": "Bool", + "name": "Bool", + "schema": "boolean" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:Date;1", + "@type": "Telemetry", + "displayName": "Date", + "name": "Date", + "schema": "date" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:DateTime;1", + "@type": "Telemetry", + "displayName": "DateTime", + "name": "DateTime", + "schema": "dateTime" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:Double;1", + "@type": "Telemetry", + "displayName": "Double", + "name": "Double", + "schema": "double" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:Duration;1", + "@type": "Telemetry", + "displayName": "Duration", + "name": "Duration", + "schema": "duration" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:IntEnum;1", + "@type": "Telemetry", + "displayName": "IntEnum", + "name": "IntEnum", + "schema": { + "@id": "dtmi:sampleApp:modelOneg4:IntEnum:pgkbdhard;1", + "@type": "Enum", + "displayName": "Enum", + "enumValues": [ + { + "@id": "dtmi:sampleApp:modelOneg4:IntEnum:pgkbdhard:Enum1;1", + "displayName": "Enum1", + "enumValue": 1, + "name": "Enum1" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:IntEnum:pgkbdhard:Enum2;1", + "displayName": "Enum2", + "enumValue": 2, + "name": "Enum2" + } + ], + "valueSchema": "integer" + } + }, + { + "@id": "dtmi:sampleApp:modelOneg4:StringEnum;1", + "@type": "Telemetry", + "displayName": "StringEnum", + "name": "StringEnum", + "schema": { + "@id": "dtmi:sampleApp:modelOneg4:StringEnum:kyesuinpsx;1", + "@type": "Enum", + "displayName": "Enum", + "enumValues": [ + { + "@id": "dtmi:sampleApp:modelOneg4:StringEnum:kyesuinpsx:EnumA;1", + "displayName": "EnumA", + "enumValue": "A", + "name": "EnumA" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:StringEnum:kyesuinpsx:EnumB;1", + "displayName": "EnumB", + "enumValue": "B", + "name": "EnumB" + } + ], + "valueSchema": "string" + } + }, + { + "@id": "dtmi:sampleApp:modelOneg4:Float;1", + "@type": "Telemetry", + "displayName": "Float", + "name": "Float", + "schema": "float" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:Geopoint;1", + "@type": "Telemetry", + "displayName": "Geopoint", + "name": "Geopoint", + "schema": "geopoint" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:Int;1", + "@type": "Telemetry", + "displayName": "Int", + "name": "Int", + "schema": "integer" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:Long;1", + "@type": "Telemetry", + "displayName": "Long", + "name": "Long", + "schema": "long" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:String;1", + "@type": "Telemetry", + "displayName": "String", + "name": "String", + "schema": "string" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:Time;1", + "@type": "Telemetry", + "displayName": "Time", + "name": "Time", + "schema": "time" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:Vector;1", + "@type": "Telemetry", + "displayName": "Vector", + "name": "Vector", + "schema": "vector" + }, + { + "@id": "dtmi:sampleApp:modelOneg4:synccmd;1", + "@type": "Command", + "commandType": "synchronous", + "displayName": "synccmd", + "name": "synccmd", + "request": { + "@type": "CommandPayload", + "displayName": "argument", + "name": "argument", + "schema": "string" + }, + "response": { + "@type": "CommandPayload", + "displayName": "status", + "name": "status", + "schema": "double" + }, + "durable": false + } + ], + "displayName": "Interface" + } ] }, - "solutionModel": { - "@id": "urn:d9cltbeus:lz1tl4a_jz", - "@type": [ - "SolutionModel" - ], - "cloudProperties": [], - "initialValues": [], - "overrides": [] - } + "@id": "dtmi:ulu38i3jr:intTestDeviceTemplateid", + "@type": [ + "DeviceModel", + "ModelDefinition" + ], + "@context": [ + "dtmi:iotcentral:context;2", + "dtmi:dtdl:context;2" + ] } \ No newline at end of file diff --git a/azext_iot/tests/central/json/property_validation_template.json b/azext_iot/tests/central/json/property_validation_template.json index ce0bee64b..4e9a4178c 100644 --- a/azext_iot/tests/central/json/property_validation_template.json +++ b/azext_iot/tests/central/json/property_validation_template.json @@ -131,138 +131,106 @@ "writable": true } ], - "implements": [ + "extends": [ { "@id": "urn:sampleApp:groupOne_bz:_rpgcmdpo:1", - "@type": [ - "InterfaceInstance" - ], - "displayName": "Interface", - "name": "groupOne_g4", - "schema": { - "@id": "urn:sampleApp:groupOne_g4:1", - "@type": [ - "Interface" - ], - "displayName": "Interface", - "contents": [ - { - "@id": "urn:sampleApp:groupOne_g4:Model:1", - "@type": [ - "Property" - ], - "displayName": "Model", - "name": "Model", - "schema": "string" - }, - { - "@id": "urn:sampleApp:groupOne_g4:Version:1", - "@type": [ - "Property" - ], - "displayName": "Version", - "name": "Version", - "schema": "string" - }, - { - "@id": "urn:sampleApp:groupOne_g4:TotalStorage:1", - "@type": [ - "Property" - ], - "displayName": "TotalStorage", - "name": "TotalStorage", - "schema": "string" - } - ] - } + "@type": "InterfaceInstance", + "contents": [ + { + "@id": "urn:sampleApp:groupOne_g4:Model:1", + "@type": [ + "Property" + ], + "displayName": "Model", + "name": "Model", + "schema": "string" + }, + { + "@id": "urn:sampleApp:groupOne_g4:Version:1", + "@type": [ + "Property" + ], + "displayName": "Version", + "name": "Version", + "schema": "string" + }, + { + "@id": "urn:sampleApp:groupOne_g4:TotalStorage:1", + "@type": [ + "Property" + ], + "displayName": "TotalStorage", + "name": "TotalStorage", + "schema": "string" + } + ] }, { "@id": "urn:sampleApp:groupTwo_bz:myxqftpsr:2", - "@type": [ - "InterfaceInstance" - ], + "@type": "InterfaceInstance", "displayName": "Interface", - "name": "groupTwo_ed", - "schema": { - "@id": "urn:sampleApp:groupTwo_ed:1", - "@type": [ - "Interface" - ], - "displayName": "Interface", - "contents": [ - { - "@id": "urn:sampleApp:groupTwo_ed:Model:1", - "@type": [ - "Property" - ], - "displayName": "Model", - "name": "Model", - "schema": "string" - }, - { - "@id": "urn:sampleApp:groupThree_ed:Manufacturer:1", - "@type": [ - "Property" - ], - "displayName": "Manufacturer", - "name": "Manufacturer", - "schema": "string" - } - ] - } + "contents": [ + { + "@id": "urn:sampleApp:groupTwo_ed:Model:1", + "@type": [ + "Property" + ], + "displayName": "Model", + "name": "Model", + "schema": "string" + }, + { + "@id": "urn:sampleApp:groupThree_ed:Manufacturer:1", + "@type": [ + "Property" + ], + "displayName": "Manufacturer", + "name": "Manufacturer", + "schema": "string" + } + ] }, { "@id": "urn:sampleApp:groupThree_bz:myxqftpsr:2", - "@type": [ - "InterfaceInstance" - ], - "displayName": "Interface", - "name": "groupThree_ed", - "schema": { - "@id": "urn:sampleApp:groupThree_ed:1", - "@type": [ - "Interface" - ], - "displayName": "Interface", - "contents": [ - { - "@id": "urn:sampleApp:groupThree_ed:Manufacturer:1", - "@type": [ - "Property" - ], - "displayName": "Manufacturer", - "name": "Manufacturer", - "schema": "string" - }, - { - "@id": "urn:sampleApp:groupThree_g4:Version:1", - "@type": [ - "Property" - ], - "displayName": "Version", - "name": "Version", - "schema": "string" - }, - { - "@id": "urn:sampleApp:groupThree_ed:Model:1", - "@type": [ - "Property" - ], - "displayName": "Model", - "name": "Model", - "schema": "string" - }, - { - "@id": "urn:sampleApp:groupThree_ed:OsName:1", - "@type": [ - "Property" - ], - "displayName": "OsName", - "name": "OsName", - "schema": "string" - } - ] - } + "@type": "Interface", + "contents": [ + { + "@id": "urn:sampleApp:groupThree_ed:Manufacturer:1", + "@type": [ + "Property" + ], + "displayName": "Manufacturer", + "name": "Manufacturer", + "schema": "string" + }, + { + "@id": "urn:sampleApp:groupThree_g4:Version:1", + "@type": [ + "Property" + ], + "displayName": "Version", + "name": "Version", + "schema": "string" + }, + { + "@id": "urn:sampleApp:groupThree_ed:Model:1", + "@type": [ + "Property" + ], + "displayName": "Model", + "name": "Model", + "schema": "string" + }, + { + "@id": "urn:sampleApp:groupThree_ed:OsName:1", + "@type": [ + "Property" + ], + "displayName": "OsName", + "name": "OsName", + "schema": "string" + } + ] } ], "displayName": "property_validation", diff --git a/azext_iot/tests/central/test_iot_central_int.py b/azext_iot/tests/central/test_iot_central_int.py index 0c16519df..d5fde866d 100644 --- a/azext_iot/tests/central/test_iot_central_int.py +++ b/azext_iot/tests/central/test_iot_central_int.py @@ -25,10 +25,7 @@ DEVICE_ID = os.environ.get("azext_iot_central_device_id") TOKEN = os.environ.get("azext_iot_central_token") DNS_SUFFIX = os.environ.get("azext_iot_central_dns_suffix") - -device_template_path = get_context_path( - __file__, "json/device_template_int_test.json" -) +device_template_path = get_context_path(__file__, "json/device_template_int_test.json") sync_command_params = get_context_path(__file__, "json/sync_command_args.json") if not all([APP_ID]): @@ -77,7 +74,7 @@ def test_central_device_twin_show_fail(self): def test_central_device_twin_show_success(self): (template_id, _) = self._create_device_template() - (device_id, _) = self._create_device(instance_of=template_id, simulated=True) + (device_id, _) = self._create_device(template=template_id, simulated=True) # wait about a few seconds for simulator to kick in so that provisioning completes time.sleep(60) @@ -101,7 +98,7 @@ def test_central_device_twin_show_success(self): def test_central_monitor_events(self): (template_id, _) = self._create_device_template() - (device_id, _) = self._create_device(instance_of=template_id) + (device_id, _) = self._create_device(template=template_id) credentials = self._get_credentials(device_id) device_client = helpers.dps_connect_device(device_id, credentials) @@ -134,7 +131,7 @@ def test_central_monitor_events(self): def test_central_validate_messages_success(self): (template_id, _) = self._create_device_template() - (device_id, _) = self._create_device(instance_of=template_id) + (device_id, _) = self._create_device(template=template_id) credentials = self._get_credentials(device_id) device_client = helpers.dps_connect_device(device_id, credentials) @@ -192,7 +189,7 @@ def test_device_connect(self): def test_central_validate_messages_issues_detected(self): expected_messages = [] (template_id, _) = self._create_device_template() - (device_id, _) = self._create_device(instance_of=template_id) + (device_id, _) = self._create_device(template=template_id) credentials = self._get_credentials(device_id) device_client = helpers.dps_connect_device(device_id, credentials) @@ -267,12 +264,13 @@ def test_central_validate_messages_issues_detected(self): assert issue in output def test_central_device_methods_CRD(self): + (device_id, device_name) = self._create_device() self.cmd( "iot central device show --app-id {} -d {}".format(APP_ID, device_id), checks=[ - self.check("approved", True), + self.check("enabled", True), self.check("displayName", device_name), self.check("id", device_id), self.check("simulated", False), @@ -332,22 +330,23 @@ def test_central_device_template_methods_CRD(self): # currently: create, show, list, delete (template_id, template_name) = self._create_device_template() - self.cmd( + result = self.cmd( "iot central device-template show --app-id {} --device-template-id {}".format( APP_ID, template_id ), - checks=[ - self.check("displayName", template_name), - self.check("id", template_id), - ], + checks=[self.check("displayName", template_name)], ) + json_result = result.get_output_in_json() + + assert json_result["@id"] == template_id + self._delete_device_template(template_id) def test_central_device_registration_info_registered(self): (template_id, _) = self._create_device_template() (device_id, device_name) = self._create_device( - instance_of=template_id, simulated=False + template=template_id, simulated=False ) result = self.cmd( @@ -374,7 +373,7 @@ def test_central_device_registration_info_registered(self): assert device_registration_info.get("device_status") == "registered" assert device_registration_info.get("id") == device_id assert device_registration_info.get("display_name") == device_name - assert device_registration_info.get("instance_of") == template_id + assert device_registration_info.get("template") == template_id assert not device_registration_info.get("simulated") # Validation - dps state @@ -384,10 +383,10 @@ def test_central_device_registration_info_registered(self): assert dps_state.get("error") == "Device is not yet provisioned." def test_central_run_command(self): - interface_id = "modelOne_g4" - command_name = "sync_cmd" + interface_id = "dtmiIntTestDeviceTemplateV33jl" + command_name = "testCommand" (template_id, _) = self._create_device_template() - (device_id, _) = self._create_device(instance_of=template_id, simulated=True) + (device_id, _) = self._create_device(template=template_id, simulated=True) self._wait_for_provisioned(device_id) @@ -451,7 +450,7 @@ def test_central_device_registration_info_unassociated(self): assert device_registration_info.get("device_status") == "unassociated" assert device_registration_info.get("id") == device_id assert device_registration_info.get("display_name") == device_name - assert device_registration_info.get("instance_of") is None + assert device_registration_info.get("template") is None assert not device_registration_info.get("simulated") # Validation - dps state @@ -495,7 +494,8 @@ def test_central_device_should_start_failover_and_failback(self): # connect & disconnect device & wait to be provisioned self._connect_gettwin_disconnect_wait_tobeprovisioned(device_id, credentials) command = "iot central device manual-failover --app-id {} --device-id {} --ttl {}".format( - APP_ID, device_id, 5) + APP_ID, device_id, 5 + ) command = self._appendOptionalArgsToCommand(command, TOKEN, DNS_SUFFIX) @@ -510,7 +510,8 @@ def test_central_device_should_start_failover_and_failback(self): self._connect_gettwin_disconnect_wait_tobeprovisioned(device_id, credentials) command = "iot central device manual-failback --app-id {} --device-id {}".format( - APP_ID, device_id) + APP_ID, device_id + ) command = self._appendOptionalArgsToCommand(command, TOKEN, DNS_SUFFIX) @@ -528,8 +529,7 @@ def test_central_device_should_start_failover_and_failback(self): "iot central device manual-failover" " --app-id {}" " --device-id {}" - " --ttl {}" - .format(APP_ID, device_id, 5) + " --ttl {}".format(APP_ID, device_id, 5) ) command = self._appendOptionalArgsToCommand(command, TOKEN, DNS_SUFFIX) @@ -556,20 +556,21 @@ def _create_device(self, **kwargs) -> (str, str): device_name = self.create_random_name(prefix="aztest", length=24) command = "iot central device create --app-id {} -d {} --device-name {}".format( - APP_ID, device_id, device_name) + APP_ID, device_id, device_name + ) command = self._appendOptionalArgsToCommand(command, TOKEN, DNS_SUFFIX) checks = [ - self.check("approved", True), + self.check("enabled", True), self.check("displayName", device_name), self.check("id", device_id), ] - instance_of = kwargs.get("instance_of") - if instance_of: - command = command + " --instance-of {}".format(instance_of) - checks.append(self.check("instanceOf", instance_of)) + template = kwargs.get("template") + if template: + command = command + " --template {}".format(template) + checks.append(self.check("template", template)) simulated = bool(kwargs.get("simulated")) if simulated: @@ -593,7 +594,7 @@ def _create_users(self,): checks = [ self.check("id", user_id), self.check("email", email), - self.check("type", "EmailUser"), + self.check("type", "email"), self.check("roles[0].role", role.value), ] users.append(self.cmd(command, checks=checks).get_output_in_json()) @@ -632,8 +633,7 @@ def _delete_api_token(self, token_id) -> None: ) def _wait_for_provisioned(self, device_id): - command = "iot central device show --app-id {} -d {}".format( - APP_ID, device_id) + command = "iot central device show --app-id {} -d {}".format(APP_ID, device_id) command = self._appendOptionalArgsToCommand(command, TOKEN, DNS_SUFFIX) while True: @@ -650,7 +650,8 @@ def _wait_for_provisioned(self, device_id): def _delete_device(self, device_id) -> None: command = "iot central device delete --app-id {} -d {} ".format( - APP_ID, device_id) + APP_ID, device_id + ) command = self._appendOptionalArgsToCommand(command, TOKEN, DNS_SUFFIX) self.cmd(command, checks=[self.check("result", "success")]) @@ -662,19 +663,15 @@ def _create_device_template(self): template_name = template["displayName"] template_id = template_name + "id" - command = "iot central device-template create --app-id {} --device-template-id {} -k '{}'".format( - APP_ID, template_id, device_template_path - ) - command = self._appendOptionalArgsToCommand(command, TOKEN, DNS_SUFFIX) - - self.cmd( - command, - checks=[ - self.check("displayName", template_name), - self.check("id", template_id), - ], + result = self.cmd( + "iot central device-template create --app-id {} --device-template-id {} -k '{}'".format( + APP_ID, template_id, device_template_path + ), + checks=[self.check("displayName", template_name)], ) + json_result = result.get_output_in_json() + assert json_result["@id"] == template_id return (template_id, template_name) def _delete_device_template(self, template_id): @@ -747,9 +744,9 @@ def _connect_gettwin_disconnect_wait_tobeprovisioned(self, device_id, credential self._wait_for_provisioned(device_id) def _appendOptionalArgsToCommand(self, command: str, token: str, dnsSuffix: str): - if token : - command = command + " --token \"{}\"".format(token) - if dnsSuffix : - command = command + " --central-dns-suffix \"{}\"".format(dnsSuffix) + if token: + command = command + ' --token "{}"'.format(token) + if dnsSuffix: + command = command + ' --central-dns-suffix "{}"'.format(dnsSuffix) return command diff --git a/azext_iot/tests/central/test_iot_central_unit.py b/azext_iot/tests/central/test_iot_central_unit.py index 10eb398c7..3d4c42fab 100644 --- a/azext_iot/tests/central/test_iot_central_unit.py +++ b/azext_iot/tests/central/test_iot_central_unit.py @@ -15,12 +15,12 @@ from azext_iot.central import commands_device_twin from azext_iot.central import commands_device from azext_iot.central import commands_monitor -from azext_iot.central.providers import ( - CentralDeviceProvider, - CentralDeviceTemplateProvider, +from azext_iot.central.providers.v1 import ( + CentralDeviceProviderV1, + CentralDeviceTemplateProviderV1, ) from azext_iot.central.models.devicetwin import DeviceTwin -from azext_iot.central.models.template import Template +from azext_iot.central import models as central_models from azext_iot.monitor.property import PropertyMonitor from azext_iot.monitor.models.enum import Severity from azext_iot.tests.helpers import load_json @@ -161,7 +161,7 @@ class TestCentralDeviceProvider: @mock.patch("azext_iot.central.services.device") def test_should_return_device(self, mock_device_svc, mock_device_template_svc): # setup - provider = CentralDeviceProvider(cmd=None, app_id=app_id) + provider = CentralDeviceProviderV1(cmd=None, app_id=app_id) mock_device_svc.get_device.return_value = self._device mock_device_template_svc.get_device_template.return_value = ( self._device_template @@ -184,7 +184,7 @@ def test_should_return_device_template( self, mock_device_svc, mock_device_template_svc ): # setup - provider = CentralDeviceTemplateProvider(cmd=None, app_id=app_id) + provider = CentralDeviceTemplateProviderV1(cmd=None, app_id=app_id) mock_device_svc.get_device.return_value = self._device mock_device_template_svc.get_device_template.return_value = ( self._device_template @@ -282,7 +282,7 @@ def test_validate_properties_declared_multiple_interfaces( ): # setup - mock_device_template_svc.get_device_template.return_value = Template( + mock_device_template_svc.get_device_template.return_value = central_models.TemplateV1( self._duplicate_property_template ) @@ -323,7 +323,7 @@ def test_validate_properties_name_miss_under_interface( ): # setup - mock_device_template_svc.get_device_template.return_value = Template( + mock_device_template_svc.get_device_template.return_value = central_models.TemplateV1( self._duplicate_property_template ) @@ -360,7 +360,7 @@ def test_validate_properties_severity_level( ): # setup - mock_device_template_svc.get_device_template.return_value = Template( + mock_device_template_svc.get_device_template.return_value = central_models.TemplateV1( self._duplicate_property_template ) @@ -405,7 +405,7 @@ def test_validate_properties_name_miss_under_component( ): # setup - mock_device_template_svc.get_device_template.return_value = Template( + mock_device_template_svc.get_device_template.return_value = central_models.TemplateV1( self._duplicate_property_template ) diff --git a/azext_iot/tests/central/test_iot_central_validator_unit.py b/azext_iot/tests/central/test_iot_central_validator_unit.py index 75bafe4bc..7fcf05fb2 100644 --- a/azext_iot/tests/central/test_iot_central_validator_unit.py +++ b/azext_iot/tests/central/test_iot_central_validator_unit.py @@ -7,7 +7,7 @@ import pytest import collections -from azext_iot.central.models.template import Template +from azext_iot.central import models as central_models from azext_iot.monitor.central_validator import validate, extract_schema_type from azext_iot.tests.helpers import load_json @@ -22,7 +22,7 @@ def test_template_interface_list(self): "urn:sampleApp:groupThree_bz:myxqftpsr:2", "urn:sampleApp:groupOne_bz:2", ] - template = Template( + template = central_models.TemplateV1( load_json(FileNames.central_property_validation_template_file) ) @@ -35,7 +35,7 @@ def test_template_component_list(self): "_rpgcmdpo", "RS40OccupancySensorV36fy", ] - template = Template( + template = central_models.TemplateV1( load_json(FileNames.central_property_validation_template_file) ) @@ -52,7 +52,7 @@ def test_extract_schema_type_component(self): "component1PropReadonly": "boolean", "component1Prop2": "boolean", } - template = Template( + template = central_models.TemplateV1( load_json(FileNames.central_property_validation_template_file) ) for key, val in expected_mapping.items(): @@ -67,7 +67,7 @@ def test_extract_schema_type_component_identifier(self): "testComponent": "boolean", "component2PropReadonly": "boolean", } - template = Template( + template = central_models.TemplateV1( load_json(FileNames.central_property_validation_template_file) ) for key, val in expected_mapping.items(): @@ -94,7 +94,9 @@ def test_extract_schema_type(self): "Time": "time", "Vector": "vector", } - template = Template(load_json(FileNames.central_device_template_file)) + template = central_models.TemplateV1( + load_json(FileNames.central_device_template_file) + ) for key, val in expected_mapping.items(): schema = template.get_schema(key) schema_type = extract_schema_type(schema) @@ -243,7 +245,9 @@ class TestComplexType: [(1, True), (2, True), (3, False), ("1", False), ("2", False)], ) def test_int_enum(self, value, expected_result): - template = Template(load_json(FileNames.central_device_template_file)) + template = central_models.TemplateV1( + load_json(FileNames.central_device_template_file) + ) schema = template.get_schema("IntEnum") assert validate(schema, value) == expected_result @@ -252,7 +256,9 @@ def test_int_enum(self, value, expected_result): [("A", True), ("B", True), ("C", False), (1, False), (2, False)], ) def test_str_enum(self, value, expected_result): - template = Template(load_json(FileNames.central_device_template_file)) + template = central_models.TemplateV1( + load_json(FileNames.central_device_template_file) + ) schema = template.get_schema("StringEnum") assert validate(schema, value) == expected_result @@ -266,7 +272,9 @@ def test_str_enum(self, value, expected_result): ], ) def test_object_simple(self, value, expected_result): - template = Template(load_json(FileNames.central_device_template_file)) + template = central_models.TemplateV1( + load_json(FileNames.central_device_template_file) + ) schema = template.get_schema("Object") assert validate(schema, value) == expected_result @@ -282,7 +290,7 @@ def test_object_simple(self, value, expected_result): ], ) def test_object_medium(self, value, expected_result): - template = Template( + template = central_models.TemplateV1( load_json(FileNames.central_deeply_nested_device_template_file) ) schema = template.get_schema("RidiculousObject") @@ -361,7 +369,7 @@ def test_object_medium(self, value, expected_result): ], ) def test_object_deep(self, value, expected_result): - template = Template( + template = central_models.TemplateV1( load_json(FileNames.central_deeply_nested_device_template_file) ) schema = template.get_schema("RidiculousObject") diff --git a/azext_iot/tests/conftest.py b/azext_iot/tests/conftest.py index 13b39f62b..aded184ab 100644 --- a/azext_iot/tests/conftest.py +++ b/azext_iot/tests/conftest.py @@ -14,6 +14,7 @@ from azure.cli.core.commands import AzCliCommand from azure.cli.core.mock import DummyCli from azext_iot.tests.generators import generate_generic_id +from azext_iot.common.shared import DeviceAuthApiType path_get_device = "azext_iot.operations.hub._iot_device_show" path_iot_hub_service_factory = "azext_iot._factory.iot_hub_service_factory" @@ -27,6 +28,7 @@ path_iot_hub_monitor_events_entrypoint = ( "azext_iot.operations.hub._iot_hub_monitor_events" ) +path_iot_device_show = "azext_iot.operations.hub._iot_device_show" hub_entity = "myhub.azure-devices.net" instance_name = generate_generic_id() @@ -169,6 +171,72 @@ def fixture_monitor_events_entrypoint(mocker): return mocker.patch(path_iot_hub_monitor_events_entrypoint) +@pytest.fixture() +def fixture_iot_device_show_sas(mocker): + device = mocker.patch(path_iot_device_show) + device.return_value = { + "authentication": { + "symmetricKey": { + "primaryKey": "test_pk", + "secondaryKey": "test_sk" + }, + "type": DeviceAuthApiType.sas.value, + "x509Thumbprint": { + "primaryThumbprint": None, + "secondaryThumbprint": None + } + }, + "capabilities": { + "iotEdge": False + }, + "cloudToDeviceMessageCount": 0, + "connectionState": "Disconnected", + "connectionStateUpdatedTime": "2021-05-27T00:36:11.2861732Z", + "deviceId": "Test_Device_1", + "etag": "ODgxNTgwOA==", + "generationId": "637534345627501371", + "hub": "test-iot-hub.azure-devices.net", + "lastActivityTime": "2021-05-27T00:18:16.3154299Z", + "status": "enabled", + "statusReason": None, + "statusUpdatedTime": "0001-01-01T00:00:00Z" + } + return device + + +@pytest.fixture() +def fixture_self_signed_device_show_self_signed(mocker): + device = mocker.patch(path_iot_device_show) + device.return_value = { + "authentication": { + "symmetricKey": { + "primaryKey": "test_pk", + "secondaryKey": "test_sk" + }, + "type": DeviceAuthApiType.selfSigned.value, + "x509Thumbprint": { + "primaryThumbprint": None, + "secondaryThumbprint": None + } + }, + "capabilities": { + "iotEdge": False + }, + "cloudToDeviceMessageCount": 0, + "connectionState": "Disconnected", + "connectionStateUpdatedTime": "2021-05-27T00:36:11.2861732Z", + "deviceId": "Test_Device_1", + "etag": "ODgxNTgwOA==", + "generationId": "637534345627501371", + "hub": "test-iot-hub.azure-devices.net", + "lastActivityTime": "2021-05-27T00:18:16.3154299Z", + "status": "enabled", + "statusReason": None, + "statusUpdatedTime": "0001-01-01T00:00:00Z" + } + return device + + # TODO: To be deprecated asap. Leverage mocked_response fixture for this functionality. def build_mock_response( mocker=None, status_code=200, payload=None, headers=None, **kwargs diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py index e36950edb..fcb978a9b 100644 --- a/azext_iot/tests/digitaltwins/__init__.py +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -160,23 +160,22 @@ def tearDown(self): # Needed because the DT service will indicate provisioning is finished before it actually is. def wait_for_hostname( - self, instance: dict, wait_in_sec: int = 10, interval: int = 4 + self, instance: dict, wait_in_sec: int = 10, interval: int = 7 ): from time import sleep - - while interval >= 1: - logger.info( - "Waiting :{} (sec) for provisioning to complete.".format(wait_in_sec) + sleep(wait_in_sec) + + self.embedded_cli.invoke( + "dt wait -n {} -g {} --custom \"hostName && provisioningState=='Succeeded'\" --interval {} --timeout {}".format( + instance["name"], + instance["resourceGroup"], + wait_in_sec, + wait_in_sec * interval ) - sleep(wait_in_sec) - interval = interval - 1 - refereshed_instance = self.embedded_cli.invoke( - "dt show -n {} -g {}".format( - instance["name"], instance["resourceGroup"] - ) - ).as_json() - - if refereshed_instance.get("hostName") and refereshed_instance["provisioningState"] == "Succeeded": - return refereshed_instance - - return instance + ) + refereshed_instance = self.embedded_cli.invoke( + "dt show -n {} -g {}".format( + instance["name"], instance["resourceGroup"] + ) + ).as_json() + return refereshed_instance if refereshed_instance else instance diff --git a/azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py index 706919f40..5854627c5 100644 --- a/azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py @@ -158,6 +158,26 @@ def test_dt_privatelinks(self): instance_name, self.rg, instance_connection_id, random_desc_rejected ) ).get_output_in_json() + + assert ( + set_connection_output["properties"]["privateLinkServiceConnectionState"]["status"] + == "Rejected" + ) + assert ( + set_connection_output["properties"]["privateLinkServiceConnectionState"]["description"] + == random_desc_rejected + ) + + self.cmd("dt network private-endpoint connection wait -n {} -g {} --cn {} --updated --interval 1 --timeout 30".format( + instance_name, self.rg, instance_connection_id + )) + + set_connection_output = self.cmd( + "dt network private-endpoint connection show -n {} -g {} --cn {}".format( + instance_name, self.rg, instance_connection_id + ) + ).get_output_in_json() + assert ( set_connection_output["properties"]["privateLinkServiceConnectionState"]["status"] == "Rejected" @@ -173,6 +193,12 @@ def test_dt_privatelinks(self): ) ) + self.cmd( + "dt network private-endpoint connection wait -n {} -g {} --cn {} --deleted --interval 1 --timeout 30".format( + instance_name, self.rg, instance_connection_id + ) + ) + list_priv_endpoints = self.cmd( "dt network private-endpoint connection list -n {} -g {}".format( instance_name, diff --git a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py index 48d50b0ea..47545158c 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -372,6 +372,15 @@ def test_dt_endpoints_routes(self): MOCK_DEAD_LETTER_SECRET, ) ).get_output_in_json() + + self.cmd( + "dt endpoint wait --created -n {} -g {} --en {} --interval 1".format( + endpoints_instance_name, + self.rg, + eventgrid_endpoint + ) + ) + assert_common_endpoint_attributes( add_ep_output, eventgrid_endpoint, @@ -407,6 +416,14 @@ def test_dt_endpoints_routes(self): ) ).get_output_in_json() + self.cmd( + "dt endpoint wait --created -n {} -g {} --en {} --interval 1".format( + endpoints_instance_name, + self.rg, + servicebus_endpoint + ) + ) + assert_common_endpoint_attributes( add_ep_sb_key_output, servicebus_endpoint, @@ -434,6 +451,14 @@ def test_dt_endpoints_routes(self): ) ).get_output_in_json() + self.cmd( + "dt endpoint wait --created -n {} -g {} --en {} --interval 1".format( + endpoints_instance_name, + self.rg, + servicebus_endpoint_msi, + ) + ) + assert_common_endpoint_attributes( add_ep_sb_identity_output, servicebus_endpoint_msi, @@ -470,6 +495,14 @@ def test_dt_endpoints_routes(self): ) ).get_output_in_json() + self.cmd( + "dt endpoint wait --created -n {} -g {} --en {} --interval 1".format( + endpoints_instance_name, + self.rg, + eventhub_endpoint, + ) + ) + assert_common_endpoint_attributes( add_ep_output, eventhub_endpoint, @@ -499,6 +532,14 @@ def test_dt_endpoints_routes(self): ) ).get_output_in_json() + self.cmd( + "dt endpoint wait -n {} -g {} --en {} --created --interval 1".format( + endpoints_instance_name, + self.rg, + eventhub_endpoint_msi, + ) + ) + assert_common_endpoint_attributes( add_ep_output, eventhub_endpoint_msi, @@ -600,10 +641,17 @@ def test_dt_endpoints_routes(self): logger.debug("Deleting endpoint {}...".format(ep.endpoint_name)) is_last = ep.endpoint_name == endpoint_tuple_collection[-1].endpoint_name self.cmd( - "dt endpoint delete -y -n {} --en {} {}".format( + "dt endpoint delete -y -n {} --en {} --no-wait {}".format( + endpoints_instance_name, + ep.endpoint_name, + "-g {}".format(self.rg) if is_last else "", + ) + ) + self.cmd( + "dt endpoint wait -n {} --en {} --deleted --interval 1 {}".format( endpoints_instance_name, ep.endpoint_name, - "-g {} --no-wait".format(self.rg) if is_last else "", + "-g {}".format(self.rg) if is_last else "", ) ) diff --git a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py index 757104999..f799d4e4e 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -516,6 +516,17 @@ def test_dt_twin(self): assert len(twin_query_result["result"]) == 0 assert twin_query_result["cost"] + self.cmd( + "dt reset -n {} --yes".format( + instance_name, + ) + ) + + model_query_result = self.cmd( + "dt model list -n {} -g {}".format(instance_name, self.rg) + ).get_output_in_json() + assert len(model_query_result) == 0 + def test_dt_twin_bulk_delete(self): self.wait_for_capacity() instance_name = generate_resource_id() @@ -685,6 +696,35 @@ def test_dt_twin_bulk_delete(self): assert len(twin_query_result["result"]) == 0 assert twin_query_result["cost"] + model_query_result = self.cmd( + "dt model list -n {} -g {}".format(instance_name, self.rg) + ).get_output_in_json() + assert len(model_query_result) > 0 + + self.cmd( + "dt twin create -n {} --dtmi {} --twin-id {}".format( + instance_name, floor_dtmi, floor_twin_id + ) + ) + + self.cmd( + "dt reset -n {} --yes".format( + instance_name, + ) + ) + + model_query_result = self.cmd( + "dt model list -n {} -g {}".format(instance_name, self.rg) + ).get_output_in_json() + assert len(model_query_result) == 0 + + twin_query_result = self.cmd( + "dt twin query -n {} -g {} -q 'select * from digitaltwins' --cost".format( + instance_name, self.rg + ) + ).get_output_in_json() + assert len(twin_query_result["result"]) == 0 + # TODO: Refactor - limited interface def assert_twin_attributes( diff --git a/azext_iot/tests/iothub/__init__.py b/azext_iot/tests/iothub/__init__.py index 55614acbf..fd73719f8 100644 --- a/azext_iot/tests/iothub/__init__.py +++ b/azext_iot/tests/iothub/__init__.py @@ -3,3 +3,22 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- + + +from azext_iot.common.certops import create_self_signed_certificate +from azext_iot.common.shared import AuthenticationTypeDataplane + +DATAPLANE_AUTH_TYPES = [ + AuthenticationTypeDataplane.key.value, + AuthenticationTypeDataplane.login.value, + "cstring", +] + +PRIMARY_THUMBPRINT = create_self_signed_certificate( + subject="aziotcli", valid_days=1, cert_output_dir=None +)["thumbprint"] +SECONDARY_THUMBPRINT = create_self_signed_certificate( + subject="aziotcli", valid_days=1, cert_output_dir=None +)["thumbprint"] + +DEVICE_TYPES = ["non-edge", "edge"] diff --git a/azext_iot/tests/iothub/configurations/test_iot_config_int.py b/azext_iot/tests/iothub/configurations/test_iot_config_int.py index 8c4997e96..8105acec6 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_int.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_int.py @@ -9,6 +9,7 @@ from azext_iot.tests import IoTLiveScenarioTest from azext_iot.tests.conftest import get_context_path +from azext_iot.tests.iothub import DATAPLANE_AUTH_TYPES from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC from azext_iot.common.utility import read_file_content @@ -37,48 +38,53 @@ def __init__(self, test_case): ) def test_edge_set_modules(self): - edge_device_count = 1 - edge_device_ids = self.generate_device_names(edge_device_count, True) - - self.cmd( - "iot hub device-identity create -d {} -n {} -g {} --ee".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG + for auth_phase in DATAPLANE_AUTH_TYPES: + edge_device_count = 1 + edge_device_ids = self.generate_device_names(edge_device_count, True) + + self.cmd( + self.set_cmd_auth_type( + "iot hub device-identity create -d {} -n {} -g {} --ee".format( + edge_device_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase, + ) ) - ) - - self.kwargs["edge_content"] = read_file_content(edge_content_path) - # Content from file - self.cmd( - "iot edge set-modules -d {} -n {} -g {} -k '{}'".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, edge_content_path - ), - checks=[self.check("length([*])", 3)], - ) + self.kwargs["edge_content"] = read_file_content(edge_content_path) - # Content inline - self.cmd( - "iot edge set-modules -d {} -n {} -g {} --content '{}'".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, "{edge_content}" - ), - self.check("length([*])", 3), - ) + # Content inline + self.cmd( + self.set_cmd_auth_type( + "iot edge set-modules -d {} -n {} -g {} --content '{}'".format( + edge_device_ids[0], LIVE_HUB, LIVE_RG, "{edge_content}" + ), + auth_type=auth_phase, + ), + self.check("length([*])", 3), + ) - # Using connection string - content from file - self.cmd( - "iot edge set-modules -d {} --login {} -k '{}'".format( - edge_device_ids[0], self.connection_string, edge_content_v1_path - ), - checks=[self.check("length([*])", 4)], - ) + # Content from file + self.cmd( + self.set_cmd_auth_type( + "iot edge set-modules -d {} -n {} -g {} -k '{}'".format( + edge_device_ids[0], LIVE_HUB, LIVE_RG, edge_content_v1_path + ), + auth_type=auth_phase, + ), + checks=[self.check("length([*])", 4)], + ) - # Error schema validation - Malformed deployment - self.cmd( - "iot edge set-modules -d {} -n {} -g {} -k '{}'".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, edge_content_malformed_path - ), - expect_failure=True, - ) + # Error schema validation - Malformed deployment + self.cmd( + self.set_cmd_auth_type( + "iot edge set-modules -d {} -n {} -g {} -k '{}'".format( + edge_device_ids[0], LIVE_HUB, LIVE_RG, edge_content_malformed_path + ), + auth_type=auth_phase + ), + expect_failure=True, + ) class TestIoTEdgeDeployments(IoTLiveScenarioTest): @@ -88,341 +94,355 @@ def __init__(self, test_case): ) def test_edge_deployments(self): - config_count = 5 - config_ids = self.generate_config_names(config_count) - - self.kwargs["generic_metrics"] = read_file_content(generic_metrics_path) - self.kwargs["edge_content"] = read_file_content(edge_content_path) - self.kwargs["edge_content_layered"] = read_file_content( - edge_content_layered_path - ) - self.kwargs["edge_content_v1"] = read_file_content(edge_content_v1_path) - self.kwargs["edge_content_malformed"] = read_file_content( - edge_content_malformed_path - ) - self.kwargs["labels"] = '{"key0": "value0"}' - - priority = random.randint(1, 10) - condition = "tags.building=9 and tags.environment='test'" - - # Content inline - # Note: $schema is included as a nested property in the sample content. - self.cmd( - """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} - --target-condition \"{}\" --labels '{}' --content '{}'""".format( - config_ids[0], - LIVE_HUB, - LIVE_RG, - priority, - condition, - "{labels}", - "{edge_content}", - ), - checks=[ - self.check("id", config_ids[0]), - self.check("priority", priority), - self.check("targetCondition", condition), - self.check("labels", json.loads(self.kwargs["labels"])), - self.check( - "content.modulesContent", - json.loads(self.kwargs["edge_content"])["content"][ - "modulesContent" - ], + for auth_phase in DATAPLANE_AUTH_TYPES: + config_count = 5 + config_ids = self.generate_config_names(config_count) + + self.kwargs["generic_metrics"] = read_file_content(generic_metrics_path) + self.kwargs["edge_content"] = read_file_content(edge_content_path) + self.kwargs["edge_content_layered"] = read_file_content( + edge_content_layered_path + ) + self.kwargs["edge_content_v1"] = read_file_content(edge_content_v1_path) + self.kwargs["edge_content_malformed"] = read_file_content( + edge_content_malformed_path + ) + self.kwargs["labels"] = '{"key0": "value0"}' + + priority = random.randint(1, 10) + condition = "tags.building=9 and tags.environment='test'" + + # Content inline + # Note: $schema is included as a nested property in the sample content. + self.cmd( + self.set_cmd_auth_type( + """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} + --target-condition \"{}\" --labels '{}' --content '{}'""".format( + config_ids[0], + LIVE_HUB, + LIVE_RG, + priority, + condition, + "{labels}", + "{edge_content}", + ), + auth_type=auth_phase, ), - self.check("metrics.queries", {}), - ], - ) + checks=[ + self.check("id", config_ids[0]), + self.check("priority", priority), + self.check("targetCondition", condition), + self.check("labels", json.loads(self.kwargs["labels"])), + self.check( + "content.modulesContent", + json.loads(self.kwargs["edge_content"])["content"][ + "modulesContent" + ], + ), + self.check("metrics.queries", {}), + ], + ) - # Using connection string - content + metrics from file. Configurations must be lowercase and will be lower()'ed. - # Note: $schema is included as a nested property in the sample content. - self.cmd( - "iot edge deployment create -d {} --login {} --pri {} --tc \"{}\" --lab '{}' -k '{}' --metrics '{}'".format( - config_ids[1].upper(), - self.connection_string, - priority, - condition, - "{labels}", - edge_content_path, - edge_content_path, - ), - checks=[ - self.check("id", config_ids[1].lower()), - self.check("priority", priority), - self.check("targetCondition", condition), - self.check("labels", json.loads(self.kwargs["labels"])), - self.check( - "content.modulesContent", - json.loads(self.kwargs["edge_content"])["content"][ - "modulesContent" - ], + # Content + metrics from file. Configurations must be lowercase and will be lower()'ed. + # Note: $schema is included as a nested property in the sample content. + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment create -d {} --pri {} --tc \"{}\" --lab '{}' -k '{}' --metrics '{}' -n {} -g {}".format( + config_ids[1].upper(), + priority, + condition, + "{labels}", + edge_content_path, + edge_content_path, + LIVE_HUB, + LIVE_RG + ), + auth_type=auth_phase ), - self.check( - "metrics.queries", - json.loads(self.kwargs["edge_content"])["metrics"]["queries"], - ), - ], - ) + checks=[ + self.check("id", config_ids[1].lower()), + self.check("priority", priority), + self.check("targetCondition", condition), + self.check("labels", json.loads(self.kwargs["labels"])), + self.check( + "content.modulesContent", + json.loads(self.kwargs["edge_content"])["content"][ + "modulesContent" + ], + ), + self.check( + "metrics.queries", + json.loads(self.kwargs["edge_content"])["metrics"]["queries"], + ), + ], + ) - # Using connection string - layered deployment with content + metrics from file. - # No labels, target-condition or priority - self.cmd( - "iot edge deployment create -d {} --login {} -k '{}' --metrics '{}' --layered".format( - config_ids[2].upper(), - self.connection_string, - edge_content_layered_path, - generic_metrics_path, - ), - checks=[ - self.check("id", config_ids[2].lower()), - self.check("priority", 0), - self.check("targetCondition", ""), - self.check("labels", None), - self.check( - "content.modulesContent", - json.loads(self.kwargs["edge_content_layered"])["content"][ - "modulesContent" - ], - ), - self.check( - "metrics.queries", - json.loads(self.kwargs["generic_metrics"])["metrics"]["queries"], + # Layered deployment with content + metrics from file. + # No labels, target-condition or priority + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment create -d {} -k '{}' --metrics '{}' --layered -n {} -g {}".format( + config_ids[2].upper(), + edge_content_layered_path, + generic_metrics_path, + LIVE_HUB, + LIVE_RG, + ), + auth_type=auth_phase ), - ], - ) + checks=[ + self.check("id", config_ids[2].lower()), + self.check("priority", 0), + self.check("targetCondition", ""), + self.check("labels", None), + self.check( + "content.modulesContent", + json.loads(self.kwargs["edge_content_layered"])["content"][ + "modulesContent" + ], + ), + self.check( + "metrics.queries", + json.loads(self.kwargs["generic_metrics"])["metrics"]["queries"], + ), + ], + ) - # Content inline - Edge v1 format - self.cmd( - """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} - --target-condition \"{}\" --labels '{}' --content '{}' --metrics '{}'""".format( - config_ids[3], - LIVE_HUB, - LIVE_RG, - priority, - condition, - "{labels}", - "{edge_content_v1}", - "{generic_metrics}", - ), - checks=[ - self.check("id", config_ids[3]), - self.check("priority", priority), - self.check("targetCondition", condition), - self.check("labels", json.loads(self.kwargs["labels"])), - self.check( - "content.modulesContent", - json.loads(self.kwargs["edge_content_v1"])["content"][ - "moduleContent" - ], - ), - self.check( - "metrics.queries", - json.loads(self.kwargs["generic_metrics"])["metrics"]["queries"], + # Content inline - Edge v1 format + self.cmd( + self.set_cmd_auth_type( + """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} + --target-condition \"{}\" --labels '{}' --content '{}' --metrics '{}'""".format( + config_ids[3], + LIVE_HUB, + LIVE_RG, + priority, + condition, + "{labels}", + "{edge_content_v1}", + "{generic_metrics}", + ), + auth_type=auth_phase, ), - ], - ) - - # Error schema validation - Malformed deployment content causes validation error - self.cmd( - """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} - --target-condition \"{}\" --labels '{}' --content '{}'""".format( - config_ids[1], - LIVE_HUB, - LIVE_RG, - priority, - condition, - "{labels}", - "{edge_content_malformed}", - ), - expect_failure=True, - ) - - # Error schema validation - Layered deployment without flag causes validation error - self.cmd( - """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} - --target-condition \"{}\" --labels '{}' --content '{}'""".format( - config_ids[1], - LIVE_HUB, - LIVE_RG, - priority, - condition, - "{labels}", - "{edge_content_layered}", - ), - expect_failure=True, - ) + checks=[ + self.check("id", config_ids[3]), + self.check("priority", priority), + self.check("targetCondition", condition), + self.check("labels", json.loads(self.kwargs["labels"])), + self.check( + "content.modulesContent", + json.loads(self.kwargs["edge_content_v1"])["content"][ + "moduleContent" + ], + ), + self.check( + "metrics.queries", + json.loads(self.kwargs["generic_metrics"])["metrics"]["queries"], + ), + ], + ) - # Uses IoT Edge hub schema version 1.1 - self.cmd( - """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} - --target-condition \"{}\" --labels '{}' --content '{}'""".format( - config_ids[4], - LIVE_HUB, - LIVE_RG, - priority, - condition, - "{labels}", - edge_content_v11_path, - ), - checks=[ - self.check("id", config_ids[4]), - self.check("priority", priority), - self.check("targetCondition", condition), - self.check("labels", json.loads(self.kwargs["labels"])), - self.check( - "content.modulesContent", - json.loads(read_file_content(edge_content_v11_path))["modulesContent"], + # Error schema validation - Malformed deployment content causes validation error + self.cmd( + self.set_cmd_auth_type( + """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} + --target-condition \"{}\" --labels '{}' --content '{}'""".format( + config_ids[1], + LIVE_HUB, + LIVE_RG, + priority, + condition, + "{labels}", + "{edge_content_malformed}", + ), + auth_type=auth_phase ), - self.check("metrics.queries", {}), - ], - ) - - # Show deployment - self.cmd( - "iot edge deployment show --deployment-id {} --hub-name {} --resource-group {}".format( - config_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("id", config_ids[0]), - self.check("priority", priority), - self.check("targetCondition", condition), - self.check("labels", json.loads(self.kwargs["labels"])), - ], - ) + expect_failure=True, + ) - # Show deployment - using connection string - self.cmd( - "iot edge deployment show -d {} --login {}".format( - config_ids[1], self.connection_string - ), - checks=[ - self.check("id", config_ids[1]), - self.check("priority", priority), - self.check("targetCondition", condition), - self.check("labels", json.loads(self.kwargs["labels"])), - ], - ) + # Error schema validation - Layered deployment without flag causes validation error + self.cmd( + self.set_cmd_auth_type( + """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} + --target-condition \"{}\" --labels '{}' --content '{}'""".format( + config_ids[1], + LIVE_HUB, + LIVE_RG, + priority, + condition, + "{labels}", + "{edge_content_layered}", + ), + auth_type=auth_phase + ), + expect_failure=True, + ) - # Update deployment - new_priority = random.randint(1, 10) - new_condition = "tags.building=43 and tags.environment='dev'" - self.kwargs["new_labels"] = '{"key": "super_value"}' - self.cmd( - "iot edge deployment update -d {} -n {} -g {} --set priority={} targetCondition=\"{}\" labels='{}'".format( - config_ids[0], - LIVE_HUB, - LIVE_RG, - new_priority, - new_condition, - "{new_labels}", - ), - checks=[ - self.check("id", config_ids[0]), - self.check("priority", new_priority), - self.check("targetCondition", new_condition), - self.check("labels", json.loads(self.kwargs["new_labels"])), - ], - ) + # Uses IoT Edge hub schema version 1.1 + self.cmd( + self.set_cmd_auth_type( + """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --priority {} + --target-condition \"{}\" --labels '{}' --content '{}'""".format( + config_ids[4], + LIVE_HUB, + LIVE_RG, + priority, + condition, + "{labels}", + edge_content_v11_path, + ), + auth_type=auth_phase, + ), + checks=[ + self.check("id", config_ids[4]), + self.check("priority", priority), + self.check("targetCondition", condition), + self.check("labels", json.loads(self.kwargs["labels"])), + self.check( + "content.modulesContent", + json.loads(read_file_content(edge_content_v11_path))["modulesContent"], + ), + self.check("metrics.queries", {}), + ], + ) - # Update deployment - using connection string - new_priority = random.randint(1, 10) - new_condition = "tags.building=40 and tags.environment='kindaprod'" - self.kwargs["new_labels"] = '{"key": "legit_value"}' - self.cmd( - "iot edge deployment update -d {} -n {} -g {} --set priority={} targetCondition=\"{}\" labels='{}'".format( - config_ids[0], - LIVE_HUB, - LIVE_RG, - new_priority, - new_condition, - "{new_labels}", - ), - checks=[ - self.check("id", config_ids[0]), - self.check("priority", new_priority), - self.check("targetCondition", new_condition), - self.check("labels", json.loads(self.kwargs["new_labels"])), - ], - ) + # Show deployment + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment show --deployment-id {} --hub-name {} --resource-group {}".format( + config_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase, + ), + checks=[ + self.check("id", config_ids[0]), + self.check("priority", priority), + self.check("targetCondition", condition), + self.check("labels", json.loads(self.kwargs["labels"])), + ], + ) - # Evaluate metrics of a deployment - user_metric_name = "mymetric" - system_metric_name = "appliedCount" - config_output = self.cmd( - "iot edge deployment show --login {} --deployment-id {}".format( - self.connection_string, config_ids[1] - ) - ).get_output_in_json() - - # Default metric type is user - self.cmd( - "iot edge deployment show-metric --metric-id {} --deployment-id {} --hub-name {}".format( - user_metric_name, config_ids[1], LIVE_HUB - ), - checks=[ - self.check("metric", user_metric_name), - self.check( - "query", config_output["metrics"]["queries"][user_metric_name] + # Update deployment + new_priority = random.randint(1, 10) + new_condition = "tags.building=43 and tags.environment='dev'" + self.kwargs["new_labels"] = '{"key": "super_value"}' + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment update -d {} -n {} -g {} --set priority={} targetCondition=\"{}\" labels='{}'".format( + config_ids[0], + LIVE_HUB, + LIVE_RG, + new_priority, + new_condition, + "{new_labels}", + ), + auth_type=auth_phase ), - ], - ) + checks=[ + self.check("id", config_ids[0]), + self.check("priority", new_priority), + self.check("targetCondition", new_condition), + self.check("labels", json.loads(self.kwargs["new_labels"])), + ], + ) - # System metric - using connection string - self.cmd( - "iot edge deployment show-metric --metric-id {} --login '{}' --deployment-id {} --metric-type {}".format( - system_metric_name, self.connection_string, config_ids[1], "system" - ), - checks=[ - self.check("metric", system_metric_name), - self.check( - "query", - config_output["systemMetrics"]["queries"][system_metric_name], + # Evaluate metrics of a deployment + user_metric_name = "mymetric" + system_metric_name = "appliedCount" + config_output = self.cmd( + self.set_cmd_auth_type( + "iot edge deployment show --deployment-id {} -n {} -g {}".format( + config_ids[1], + LIVE_HUB, + LIVE_RG + ), + auth_type=auth_phase + ) + ).get_output_in_json() + + # Default metric type is user + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment show-metric --metric-id {} --deployment-id {} --hub-name {}".format( + user_metric_name, config_ids[1], LIVE_HUB + ), + auth_type=auth_phase ), - ], - ) + checks=[ + self.check("metric", user_metric_name), + self.check( + "query", config_output["metrics"]["queries"][user_metric_name] + ), + ], + ) - # Error - metric does not exist, using connection string - self.cmd( - "iot edge deployment show-metric -m {} --login {} -d {}".format( - "doesnotexist", self.connection_string, config_ids[0] - ), - expect_failure=True, - ) + # System metric + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment show-metric --metric-id {} --deployment-id {} --metric-type {} -n {} -g {}".format( + system_metric_name, config_ids[1], "system", LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase + ), + checks=[ + self.check("metric", system_metric_name), + self.check( + "query", + config_output["systemMetrics"]["queries"][system_metric_name], + ), + ], + ) - config_list_check = [ - self.check("length([*])", config_count), - self.exists("[?id=='{}']".format(config_ids[0])), - self.exists("[?id=='{}']".format(config_ids[1])), - self.exists("[?id=='{}']".format(config_ids[2])), - self.exists("[?id=='{}']".format(config_ids[3])) - ] - - # List all edge deployments - self.cmd( - "iot edge deployment list -n {} -g {}".format(LIVE_HUB, LIVE_RG), - checks=config_list_check, - ) + # Error - metric does not exist + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment show-metric -m {} -d {} -n {} -g {}".format( + "doesnotexist", config_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase + ), + expect_failure=True, + ) - # List all edge deployments - using connection string - self.cmd( - "iot edge deployment list --login {}".format(self.connection_string), - checks=config_list_check, - ) + config_list_check = [ + self.check("length([*])", config_count), + self.exists("[?id=='{}']".format(config_ids[0])), + self.exists("[?id=='{}']".format(config_ids[1])), + self.exists("[?id=='{}']".format(config_ids[2])), + self.exists("[?id=='{}']".format(config_ids[3])) + ] + + # List all edge deployments + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment list -n {} -g {}".format(LIVE_HUB, LIVE_RG), + auth_type=auth_phase + ), + checks=config_list_check, + ) - # Explicitly delete an edge deployment - self.cmd( - "iot edge deployment delete -d {} -n {} -g {}".format( - config_ids[0], LIVE_HUB, LIVE_RG + # Explicitly delete an edge deployment + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment delete -d {} -n {} -g {}".format( + config_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase + ) ) - ) - del self.config_ids[0] - # Explicitly delete an edge deployment - using connection string - self.cmd( - "iot edge deployment delete -d {} --login {}".format( - config_ids[1], self.connection_string + # Validate deletion + self.cmd( + self.set_cmd_auth_type( + "iot edge deployment show -d {} -n {} -g {}".format( + config_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase + ), + expect_failure=True ) - ) - del self.config_ids[0] + + self.tearDown() class TestIoTHubConfigurations(IoTLiveScenarioTest): @@ -445,272 +465,284 @@ def test_device_configurations(self): priority = random.randint(1, 10) condition = "tags.building=9 and tags.environment='test'" - # Device content inline - # Note: $schema is included as a nested property in the sample content. - self.cmd( - """iot hub configuration create --config-id {} --hub-name {} --resource-group {} --priority {} - --target-condition \"{}\" --labels '{}' --content '{}'""".format( - config_ids[0], - LIVE_HUB, - LIVE_RG, - priority, - condition, - "{labels}", - "{adm_content_device}", - ), - checks=[ - self.check("id", config_ids[0]), - self.check("priority", priority), - self.check("targetCondition", condition), - self.check("labels", json.loads(self.kwargs["labels"])), - self.check( - "content.deviceContent", - json.loads(self.kwargs["adm_content_device"])["content"][ - "deviceContent" - ], + for auth_phase in DATAPLANE_AUTH_TYPES: + # Device content inline + # Note: $schema is included as a nested property in the sample content. + self.cmd( + self.set_cmd_auth_type( + """iot hub configuration create --config-id {} --hub-name {} --resource-group {} --priority {} + --target-condition \"{}\" --labels '{}' --content '{}'""".format( + config_ids[0], + LIVE_HUB, + LIVE_RG, + priority, + condition, + "{labels}", + "{adm_content_device}", + ), + auth_type=auth_phase ), - self.check("metrics.queries", {}), - ], - ) + checks=[ + self.check("id", config_ids[0]), + self.check("priority", priority), + self.check("targetCondition", condition), + self.check("labels", json.loads(self.kwargs["labels"])), + self.check( + "content.deviceContent", + json.loads(self.kwargs["adm_content_device"])["content"][ + "deviceContent" + ], + ), + self.check("metrics.queries", {}), + ], + ) - # Using connection string - module content + metrics from file. Configurations must be lowercase and will be lower()'ed. - # Note: $schema is included as a nested property in the sample content. - module_condition = "{} {}".format("FROM devices.modules WHERE", condition) - self.cmd( - "iot hub configuration create -c {} --login {} --pri {} --tc \"{}\" --lab '{}' -k '{}' --metrics '{}'".format( - config_ids[1].upper(), - self.connection_string, - priority, - module_condition, - "{labels}", - adm_content_module_path, - adm_content_module_path, - ), - checks=[ - self.check("id", config_ids[1].lower()), - self.check("priority", priority), - self.check("targetCondition", module_condition), - self.check("labels", json.loads(self.kwargs["labels"])), - self.check( - "content.moduleContent", - json.loads(self.kwargs["adm_content_module"])["content"][ - "moduleContent" - ], - ), - self.check( - "metrics.queries", - json.loads(self.kwargs["adm_content_module"])["metrics"]["queries"], + # Module content + metrics from file. + # Configurations must be lowercase and will be lower()'ed. + # Note: $schema is included as a nested property in the sample content. + module_condition = "{} {}".format("FROM devices.modules WHERE", condition) + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration create -c {} --pri {} --tc \"{}\" --lab '{}' -k '{}' -m '{}' -n {} -g {}".format( + config_ids[1].upper(), + priority, + module_condition, + "{labels}", + adm_content_module_path, + adm_content_module_path, + LIVE_HUB, + LIVE_RG + ), + auth_type=auth_phase, ), - ], - ) + checks=[ + self.check("id", config_ids[1].lower()), + self.check("priority", priority), + self.check("targetCondition", module_condition), + self.check("labels", json.loads(self.kwargs["labels"])), + self.check( + "content.moduleContent", + json.loads(self.kwargs["adm_content_module"])["content"][ + "moduleContent" + ], + ), + self.check( + "metrics.queries", + json.loads(self.kwargs["adm_content_module"])["metrics"]["queries"], + ), + ], + ) - # Using connection string - device content + metrics from file. Configurations must be lowercase and will be lower()'ed. - # No labels, target-condition or priority - self.cmd( - "iot hub configuration create -c {} --login {} -k '{}' --metrics '{}'".format( - config_ids[2].upper(), - self.connection_string, - adm_content_device_path, - generic_metrics_path, - ), - checks=[ - self.check("id", config_ids[2].lower()), - self.check("priority", 0), - self.check("targetCondition", ""), - self.check("labels", None), - self.check( - "content.deviceContent", - json.loads(self.kwargs["adm_content_device"])["content"][ - "deviceContent" - ], - ), - self.check( - "metrics.queries", - json.loads(self.kwargs["generic_metrics"])["metrics"]["queries"], + # Device content + metrics from file. + # Configurations must be lowercase and will be lower()'ed. + # No labels, target-condition or priority + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration create -c {} -k '{}' --metrics '{}' -n {} -g {}".format( + config_ids[2].upper(), + adm_content_device_path, + generic_metrics_path, + LIVE_HUB, + LIVE_RG + ), + auth_type=auth_phase ), - ], - ) - - # Error validation - Malformed configuration content causes validation error - # In this case we attempt to use an edge deployment ^_^ - self.cmd( - """iot hub configuration create --config-id {} --hub-name {} --resource-group {} --priority {} - --target-condition \"{}\" --labels '{}' --content '{}'""".format( - config_ids[1], - LIVE_HUB, - LIVE_RG, - priority, - condition, - "{labels}", - "{edge_content}", - ), - expect_failure=True, - ) - - # Error validation - Module configuration target condition must start with 'from devices.modules where' - module_condition = "{} {}".format("FROM devices.modules WHERE", condition) - self.cmd( - "iot hub configuration create -c {} --login {} -k '{}'".format( - config_ids[1].upper(), - self.connection_string, - adm_content_module_path, - ), - expect_failure=True, - ) - - # Show ADM configuration - self.cmd( - "iot hub configuration show --config-id {} --hub-name {} --resource-group {}".format( - config_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("id", config_ids[0]), - self.check("priority", priority), - self.check("targetCondition", condition), - self.check("labels", json.loads(self.kwargs["labels"])), - ], - ) + checks=[ + self.check("id", config_ids[2].lower()), + self.check("priority", 0), + self.check("targetCondition", ""), + self.check("labels", None), + self.check( + "content.deviceContent", + json.loads(self.kwargs["adm_content_device"])["content"][ + "deviceContent" + ], + ), + self.check( + "metrics.queries", + json.loads(self.kwargs["generic_metrics"])["metrics"]["queries"], + ), + ], + ) - # Show ADM configuration - using connection string - self.cmd( - "iot hub configuration show -c {} --login {}".format( - config_ids[1], self.connection_string - ), - checks=[ - self.check("id", config_ids[1]), - self.check("priority", priority), - self.check("targetCondition", module_condition), - self.check("labels", json.loads(self.kwargs["labels"])), - ], - ) + # Error validation - Malformed configuration content causes validation error + # In this case we attempt to use an edge deployment ^_^ + self.cmd( + self.set_cmd_auth_type( + """iot hub configuration create --config-id {} --hub-name {} --resource-group {} --priority {} + --target-condition \"{}\" --labels '{}' --content '{}'""".format( + config_ids[1], + LIVE_HUB, + LIVE_RG, + priority, + condition, + "{labels}", + "{edge_content}", + ), + auth_type=auth_phase + ), + expect_failure=True, + ) - # Update deployment - new_priority = random.randint(1, 10) - new_condition = "tags.building=43 and tags.environment='dev'" - self.kwargs["new_labels"] = '{"key": "super_value"}' - self.cmd( - "iot hub configuration update -c {} -n {} -g {} --set priority={} targetCondition=\"{}\" labels='{}'".format( - config_ids[0], - LIVE_HUB, - LIVE_RG, - new_priority, - new_condition, - "{new_labels}", - ), - checks=[ - self.check("id", config_ids[0]), - self.check("priority", new_priority), - self.check("targetCondition", new_condition), - self.check("labels", json.loads(self.kwargs["new_labels"])), - ], - ) + # Error validation - Module configuration target condition must start with 'from devices.modules where' + module_condition = "{} {}".format("FROM devices.modules WHERE", condition) + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration create -c {} -k '{}' -n {} -g {}".format( + config_ids[1].upper(), + adm_content_module_path, + LIVE_HUB, + LIVE_RG + ), + auth_type=auth_phase + ), + expect_failure=True, + ) - # Update deployment - using connection string - new_priority = random.randint(1, 10) - new_condition = "tags.building=40 and tags.environment='kindaprod'" - self.kwargs["new_labels"] = '{"key": "legit_value"}' - self.cmd( - "iot hub configuration update -c {} -n {} -g {} --set priority={} targetCondition=\"{}\" labels='{}'".format( - config_ids[0], - LIVE_HUB, - LIVE_RG, - new_priority, - new_condition, - "{new_labels}", - ), - checks=[ - self.check("id", config_ids[0]), - self.check("priority", new_priority), - self.check("targetCondition", new_condition), - self.check("labels", json.loads(self.kwargs["new_labels"])), - ], - ) + # Show ADM configuration + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration show --config-id {} --hub-name {} --resource-group {}".format( + config_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase + ), + checks=[ + self.check("id", config_ids[0]), + self.check("priority", priority), + self.check("targetCondition", condition), + self.check("labels", json.loads(self.kwargs["labels"])), + ], + ) - # Evaluate metrics of a deployment - user_metric_name = "mymetric" - system_metric_name = "appliedCount" - config_output = self.cmd( - "iot hub configuration show --login {} --config-id {}".format( - self.connection_string, config_ids[1] - ) - ).get_output_in_json() - - # Default metric type is user - self.cmd( - "iot hub configuration show-metric --metric-id {} --config-id {} --hub-name {}".format( - user_metric_name, config_ids[1], LIVE_HUB - ), - checks=[ - self.check("metric", user_metric_name), - self.check( - "query", config_output["metrics"]["queries"][user_metric_name] + # Update deployment + new_priority = random.randint(1, 10) + new_condition = "tags.building=43 and tags.environment='dev'" + self.kwargs["new_labels"] = '{"key": "super_value"}' + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration update -c {} -n {} -g {} --set priority={} targetCondition=\"{}\" labels='{}'".format( + config_ids[0], + LIVE_HUB, + LIVE_RG, + new_priority, + new_condition, + "{new_labels}", + ), + auth_type=auth_phase, ), - ], - ) + checks=[ + self.check("id", config_ids[0]), + self.check("priority", new_priority), + self.check("targetCondition", new_condition), + self.check("labels", json.loads(self.kwargs["new_labels"])), + ], + ) - # System metric - using connection string - self.cmd( - "iot hub configuration show-metric --metric-id {} --login '{}' --config-id {} --metric-type {}".format( - system_metric_name, self.connection_string, config_ids[1], "system" - ), - checks=[ - self.check("metric", system_metric_name), - self.check( - "query", - config_output["systemMetrics"]["queries"][system_metric_name], + # Evaluate metrics of a deployment + user_metric_name = "mymetric" + system_metric_name = "appliedCount" + config_output = self.cmd( + self.set_cmd_auth_type( + "iot hub configuration show --config-id {} -n {} -g {}".format( + config_ids[1], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase, + ) + ).get_output_in_json() + + # Default metric type is user + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration show-metric --metric-id {} --config-id {} --hub-name {}".format( + user_metric_name, config_ids[1], LIVE_HUB + ), + auth_type=auth_phase ), - ], - ) + checks=[ + self.check("metric", user_metric_name), + self.check( + "query", config_output["metrics"]["queries"][user_metric_name] + ), + ], + ) - # Error - metric does not exist, using connection string - self.cmd( - "iot hub configuration show-metric -m {} --login {} -c {}".format( - "doesnotexist", self.connection_string, config_ids[0] - ), - expect_failure=True, - ) + # System metric + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration show-metric --metric-id {} --config-id {} --metric-type {} -n {} -g {}".format( + system_metric_name, config_ids[1], "system", LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase, + ), + checks=[ + self.check("metric", system_metric_name), + self.check( + "query", + config_output["systemMetrics"]["queries"][system_metric_name], + ), + ], + ) - # Create Edge deployment to ensure it doesn't show up on ADM list - self.cmd( - """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --content '{}'""".format( - edge_config_ids[0], - LIVE_HUB, - LIVE_RG, - "{edge_content}", + # Error - metric does not exist, using connection string + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration show-metric -m {} -c {} -n {} -g {}".format( + "doesnotexist", config_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase, + ), + expect_failure=True, ) - ) - config_list_check = [ - self.check("length([*])", config_count), - self.exists("[?id=='{}']".format(config_ids[0])), - self.exists("[?id=='{}']".format(config_ids[1])), - self.exists("[?id=='{}']".format(config_ids[2])) - ] - - # List all ADM configurations - self.cmd( - "iot hub configuration list -n {} -g {}".format(LIVE_HUB, LIVE_RG), - checks=config_list_check, - ) + # Create Edge deployment to ensure it doesn't show up on ADM list + self.cmd( + self.set_cmd_auth_type( + """iot edge deployment create --deployment-id {} --hub-name {} --resource-group {} --content '{}'""".format( + edge_config_ids[0], + LIVE_HUB, + LIVE_RG, + "{edge_content}", + ), + auth_type=auth_phase + ) + ) - # List all ADM configurations - using connection string - self.cmd( - "iot hub configuration list --login {}".format(self.connection_string), - checks=config_list_check, - ) + config_list_check = [ + self.check("length([*])", config_count), + self.exists("[?id=='{}']".format(config_ids[0])), + self.exists("[?id=='{}']".format(config_ids[1])), + self.exists("[?id=='{}']".format(config_ids[2])) + ] + + # List all ADM configurations + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration list -n {} -g {}".format(LIVE_HUB, LIVE_RG), + auth_type=auth_phase + ), + checks=config_list_check, + ) - # Explicitly delete an ADM configuration - self.cmd( - "iot hub configuration delete -c {} -n {} -g {}".format( - config_ids[0], LIVE_HUB, LIVE_RG + # Explicitly delete an ADM configuration + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration delete -c {} -n {} -g {}".format( + config_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase + ) ) - ) - del self.config_ids[0] - # Explicitly delete an ADM configuration - using connection string - self.cmd( - "iot hub configuration delete -c {} --login {}".format( - config_ids[1], self.connection_string + # Validate deletion + self.cmd( + self.set_cmd_auth_type( + "iot hub configuration show -c {} -n {} -g {}".format( + config_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase, + ), + expect_failure=True ) - ) - del self.config_ids[0] + + self.tearDown() diff --git a/azext_iot/tests/iothub/devices/__init__.py b/azext_iot/tests/iothub/devices/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/tests/iothub/devices/__init__.py @@ -0,0 +1,5 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/azext_iot/tests/iothub/devices/test_iothub_device_tracing.py b/azext_iot/tests/iothub/devices/test_iothub_device_tracing.py new file mode 100644 index 000000000..932f96ce4 --- /dev/null +++ b/azext_iot/tests/iothub/devices/test_iothub_device_tracing.py @@ -0,0 +1,68 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import pytest + +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.common.shared import AuthenticationTypeDataplane + +settings = DynamoSettings(req_env_set=ENV_SET_TEST_IOTHUB_BASIC) + +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg + + +# The current implementation of preview distributed tracing commands do not work with a cstring. + +custom_auth_types = [ + AuthenticationTypeDataplane.key.value, + AuthenticationTypeDataplane.login.value, +] + + +class TestIoTHubDistributedTracing(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTHubDistributedTracing, self).__init__(test_case, LIVE_HUB, LIVE_RG) + + def test_iothub_device_distributed_tracing(self): + # Region specific test + if self.region not in ["West US 2", "North Europe", "Southeast Asia"]: + pytest.skip( + msg="Skipping distributed-tracing tests. IoT Hub not in supported region!" + ) + return + + for auth_phase in custom_auth_types: + device_count = 1 + device_ids = self.generate_device_names(device_count) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub distributed-tracing show -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=self.is_empty(), + ) + + result = self.cmd( + self.set_cmd_auth_type( + f"iot hub distributed-tracing update -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --sm on --sr 50", + auth_type=auth_phase, + ) + ).get_output_in_json() + + assert result["deviceId"] == device_ids[0] + assert result["samplingMode"] == "enabled" + assert result["samplingRate"] == "50%" + assert not result["isSynced"] diff --git a/azext_iot/tests/iothub/devices/test_iothub_device_twin_int.py b/azext_iot/tests/iothub/devices/test_iothub_device_twin_int.py new file mode 100644 index 000000000..fe2692802 --- /dev/null +++ b/azext_iot/tests/iothub/devices/test_iothub_device_twin_int.py @@ -0,0 +1,207 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +import os +from pathlib import Path + +from azext_iot.common.utility import read_file_content +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.tests.generators import generate_generic_id +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.tests.iothub import DATAPLANE_AUTH_TYPES + +settings = DynamoSettings(req_env_set=ENV_SET_TEST_IOTHUB_BASIC) + +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg +CWD = os.path.dirname(os.path.abspath(__file__)) + + +class TestIoTHubDeviceTwin(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTHubDeviceTwin, self).__init__(test_case, LIVE_HUB, LIVE_RG) + + def test_iothub_device_twin(self): + for auth_phase in DATAPLANE_AUTH_TYPES: + device_count = 1 + device_ids = self.generate_device_names(device_count) + + patch_desired = { + generate_generic_id(): generate_generic_id(), + generate_generic_id(): generate_generic_id(), + } + patch_tags = { + generate_generic_id(): generate_generic_id(), + generate_generic_id(): generate_generic_id(), + } + + self.kwargs["patch_desired"] = json.dumps(patch_desired) + self.kwargs["patch_tags"] = json.dumps(patch_tags) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ) + + # Initial twin state + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin show -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", device_ids[0]), + self.exists("properties.desired"), + self.exists("properties.reported"), + ], + ).get_output_in_json() + + assert d0_twin["properties"]["desired"]["$version"] == 1 + assert d0_twin["properties"]["reported"]["$version"] == 1 + + # Patch based twin update of desired props + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin update -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--desired '{patch_desired}'", # Not f-string due to CLI TestFramework self.kwargs application :( + auth_type=auth_phase, + ) + ).get_output_in_json() + + assert d0_twin["properties"]["desired"]["$version"] == 2 + + for key in patch_desired: + assert d0_twin["properties"]["desired"][key] == patch_desired[key] + + # Patch based twin update of tag props + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin update -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--tags '{patch_tags}'", # Not f-string due to CLI TestFramework self.kwargs application :( + auth_type=auth_phase, + ) + ).get_output_in_json() + + for key in patch_tags: + assert d0_twin["tags"][key] == patch_tags[key] + + for key in patch_desired: + assert d0_twin["properties"]["desired"][key] == patch_desired[key] + + assert d0_twin["properties"]["desired"]["$version"] == 2 + + # Patch based twin update of tag and desired props + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin update -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--tags '{patch_tags}' --desired '{patch_desired}'", + auth_type=auth_phase, + ) + ).get_output_in_json() + + for key in patch_tags: + assert d0_twin["tags"][key] == patch_tags[key] + + for key in patch_desired: + assert d0_twin["properties"]["desired"][key] == patch_desired[key] + + assert d0_twin["properties"]["desired"]["$version"] == 3 + + # Prepare removal of all twin tag properties + for key in patch_tags: + patch_tags[key] = None + self.kwargs["patch_tags"] = json.dumps(patch_tags) + + # Remove all twin tag properties + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin update -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--tags '{patch_tags}'", + auth_type=auth_phase, + ) + ).get_output_in_json() + assert d0_twin["tags"] is None + + # Prepare removal of single desired twin property + target_key = list(patch_desired.keys())[0] + patch_desired[target_key] = None + self.kwargs["patch_desired"] = json.dumps(patch_desired) + + # Remove single desired property + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin update -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--desired '{patch_desired}'", + auth_type=auth_phase, + ) + ).get_output_in_json() + + assert d0_twin["properties"]["desired"].get(target_key) is None + assert d0_twin["properties"]["desired"]["$version"] == 4 + + # Validation error --desired is not an object + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin update -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--desired 'badinput'", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + def test_iothub_device_twin_replace(self): + for auth_phase in DATAPLANE_AUTH_TYPES: + device_count = 1 + device_ids = self.generate_device_names(device_count) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ) + + replace_twin_content_path = os.path.join( + Path(CWD).parent, "test_generic_replace.json" + ) + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin replace -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} -j '{replace_twin_content_path}'", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", device_ids[0]), + self.check("properties.desired.awesome", 9001), + self.check("properties.desired.temperature.min", 10), + self.check("properties.desired.temperature.max", 100), + self.check("tags.location.region", "US"), + ], + ) + + # Inline json + replace_twin_content_path = os.path.join( + Path(CWD).parent, "test_generic_replace.json" + ) + self.kwargs["inline_replace_content"] = read_file_content( + replace_twin_content_path + ) + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin replace -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "-j '{inline_replace_content}'", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", device_ids[0]), + self.check("properties.desired.awesome", 9001), + self.check("properties.desired.temperature.min", 10), + self.check("properties.desired.temperature.max", 100), + self.check("tags.location.region", "US"), + ], + ) diff --git a/azext_iot/tests/iothub/devices/test_iothub_devices_int.py b/azext_iot/tests/iothub/devices/test_iothub_devices_int.py new file mode 100644 index 000000000..dbced2f08 --- /dev/null +++ b/azext_iot/tests/iothub/devices/test_iothub_devices_int.py @@ -0,0 +1,427 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.tests.generators import generate_generic_id +from azext_iot.tests.iothub import ( + DATAPLANE_AUTH_TYPES, + PRIMARY_THUMBPRINT, + SECONDARY_THUMBPRINT, + DEVICE_TYPES, +) + +settings = DynamoSettings(req_env_set=ENV_SET_TEST_IOTHUB_BASIC) + +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg + + +class TestIoTHubDevices(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTHubDevices, self).__init__(test_case, LIVE_HUB, LIVE_RG) + + def test_iothub_device_identity(self): + to_remove_device_ids = [] + for auth_phase in DATAPLANE_AUTH_TYPES: + for device_type in DEVICE_TYPES: + device_count = 4 + device_ids = self.generate_device_names( + device_count, edge=device_type == "edge" + ) + edge_enabled = "--edge-enabled" if device_type == "edge" else "" + + # Symmetric key device checks + d0_device_checks = [ + self.check("deviceId", device_ids[0]), + self.check("status", "enabled"), + self.check("statusReason", None), + self.check("connectionState", "Disconnected"), + self.check("capabilities.iotEdge", device_type == "edge"), + self.exists("authentication.symmetricKey.primaryKey"), + self.exists("authentication.symmetricKey.secondaryKey"), + ] + + # Symmetric key device creation + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} {edge_enabled}", + auth_type=auth_phase, + ), + checks=d0_device_checks, + ) + to_remove_device_ids.append(device_ids[0]) + + # x509 thumbprint device checks + d1_device_checks = [ + self.check("deviceId", device_ids[1]), + self.check("status", "enabled"), + self.check("statusReason", None), + self.check("capabilities.iotEdge", device_type == "edge"), + self.check("connectionState", "Disconnected"), + self.check("authentication.symmetricKey.primaryKey", None), + self.check("authentication.symmetricKey.secondaryKey", None), + self.check( + "authentication.x509Thumbprint.primaryThumbprint", + PRIMARY_THUMBPRINT, + ), + self.check( + "authentication.x509Thumbprint.secondaryThumbprint", + SECONDARY_THUMBPRINT, + ), + ] + + # Create x509 thumbprint device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create --device-id {device_ids[1]} " + f"--hub-name {LIVE_HUB} --resource-group {LIVE_RG} --auth-method x509_thumbprint " + f"--primary-thumbprint {PRIMARY_THUMBPRINT} --secondary-thumbprint {SECONDARY_THUMBPRINT} " + f"{edge_enabled}", + auth_type=auth_phase, + ), + checks=d1_device_checks, + ) + to_remove_device_ids.append(device_ids[1]) + + # Create x509 thumbprint device using generated cert for primary thumbprint + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create --device-id {device_ids[2]} --hub-name {LIVE_HUB} " + f"--resource-group {LIVE_RG} --auth-method x509_thumbprint --valid-days 1 {edge_enabled}", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", device_ids[2]), + self.check("status", "enabled"), + self.check("statusReason", None), + self.check("capabilities.iotEdge", device_type == "edge"), + self.check("connectionState", "Disconnected"), + self.check("authentication.symmetricKey.primaryKey", None), + self.check("authentication.symmetricKey.secondaryKey", None), + self.exists("authentication.x509Thumbprint.primaryThumbprint"), + self.check( + "authentication.x509Thumbprint.secondaryThumbprint", + None, + ), + ], + ) + to_remove_device_ids.append(device_ids[2]) + + # Create x509 CA device, disabled status with reason, auth with connection string + status_reason = "Test Status Reason" + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create --device-id {device_ids[3]} --hub-name {LIVE_HUB} " + f"--resource-group {LIVE_RG} --auth-method x509_ca --status disabled " + f"--status-reason '{status_reason}' {edge_enabled}", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", device_ids[3]), + self.check("status", "disabled"), + self.check("statusReason", status_reason), + self.check("capabilities.iotEdge", device_type == "edge"), + self.check("connectionState", "Disconnected"), + self.check("authentication.symmetricKey.primaryKey", None), + self.check("authentication.symmetricKey.secondaryKey", None), + self.check( + "authentication.x509Thumbprint.primaryThumbprint", + None, + ), + self.check( + "authentication.x509Thumbprint.secondaryThumbprint", + None, + ), + ], + ) + + # Delete device identity + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity delete -d {device_ids[3]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=self.is_empty(), + ) + + # Validate deletion worked + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity show -d {device_ids[3]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Show symmetric key device identity + d0_show = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity show -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=d0_device_checks, + ).get_output_in_json() + + # Reset device symmetric key using device-identity generic update + d0_updated = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity update -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + '--set authentication.symmetricKey.primaryKey="" ' + 'authentication.symmetricKey.secondaryKey=""', + auth_type=auth_phase, + ) + ).get_output_in_json() + assert ( + d0_updated["authentication"]["symmetricKey"]["primaryKey"] + != d0_show["authentication"]["symmetricKey"]["primaryKey"] + ) + assert ( + d0_updated["authentication"]["symmetricKey"]["secondaryKey"] + != d0_show["authentication"]["symmetricKey"]["secondaryKey"] + ) + + # Update device identity with higher level update parms + random_status_reason = generate_generic_id() + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity update -d {device_ids[1]} --ee false " + f"--ptp {SECONDARY_THUMBPRINT} --stp {PRIMARY_THUMBPRINT} " + f"--status-reason '{random_status_reason}' --status disabled " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", device_ids[1]), + self.check("status", "disabled"), + self.check("capabilities.iotEdge", False), + self.check("statusReason", random_status_reason), + self.check( + "authentication.x509Thumbprint.primaryThumbprint", + SECONDARY_THUMBPRINT, + ), + self.check( + "authentication.x509Thumbprint.secondaryThumbprint", + PRIMARY_THUMBPRINT, + ), + ], + ) + + query_checks = [self.check("length([*])", len(to_remove_device_ids))] + for d in to_remove_device_ids: + query_checks.append(self.exists(f"[?deviceId=='{d}']")) + + # By default query has no return cap + self.cmd( + self.set_cmd_auth_type( + f'iot hub query --hub-name {LIVE_HUB} -g {LIVE_RG} -q "select * from devices"', + auth_type=auth_phase, + ), + checks=query_checks, + ) + + # -1 Top is equivalent to unlimited + self.cmd( + self.set_cmd_auth_type( + f'iot hub query --top -1 --hub-name {LIVE_HUB} -g {LIVE_RG} -q "select * from devices"', + auth_type=auth_phase, + ), + checks=query_checks, + ) + + # Explicit top to constrain records and use connection string + self.cmd( + self.set_cmd_auth_type( + f'iot hub query --top 1 --hub-name {LIVE_HUB} -g {LIVE_RG} -q "select * from devices"', + auth_type=auth_phase, + ), + checks=[self.check("length([*])", 1)], + ) + # List devices + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity list -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=query_checks, + ) + + # List devices filtering for edge devices + edge_filtered_list = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity list -n {LIVE_HUB} -g {LIVE_RG} --ee", + auth_type=auth_phase, + ) + ).get_output_in_json() + assert all( + (d["capabilities"]["iotEdge"] is True for d in edge_filtered_list) + ) + + def test_iothub_device_renew_key(self): + device_count = 2 + device_ids = self.generate_device_names(device_count) + + original_device = self.cmd( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ).get_output_in_json() + + self.cmd( + f"iot hub device-identity create -d {device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG} " + f"--am x509_thumbprint --ptp {PRIMARY_THUMBPRINT} --stp {SECONDARY_THUMBPRINT}" + ) + + for auth_phase in DATAPLANE_AUTH_TYPES: + renew_primary_key_device = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity renew-key -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --kt primary", + auth_type=auth_phase, + ) + ).get_output_in_json() + assert ( + renew_primary_key_device["authentication"]["symmetricKey"]["primaryKey"] + != original_device["authentication"]["symmetricKey"]["primaryKey"] + ) + assert ( + renew_primary_key_device["authentication"]["symmetricKey"][ + "secondaryKey" + ] + == original_device["authentication"]["symmetricKey"]["secondaryKey"] + ) + + swap_keys_device = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity renew-key -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --kt swap", + auth_type=auth_phase, + ) + ).get_output_in_json() + assert ( + renew_primary_key_device["authentication"]["symmetricKey"]["primaryKey"] + == swap_keys_device["authentication"]["symmetricKey"]["secondaryKey"] + ) + assert ( + renew_primary_key_device["authentication"]["symmetricKey"]["secondaryKey"] + == swap_keys_device["authentication"]["symmetricKey"]["primaryKey"] + ) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity renew-key -d {device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG} --kt secondary", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + def test_iothub_device_connection_string_show(self): + device_count = 2 + device_ids = self.generate_device_names(device_count) + + symmetric_key_device = self.cmd( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ).get_output_in_json() + + self.cmd( + f"iot hub device-identity create -d {device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG} --am x509_ca" + ) + + sym_cstring_pattern = f"HostName={LIVE_HUB}.azure-devices.net;DeviceId={device_ids[0]};SharedAccessKey=#" + cer_cstring_pattern = ( + f"HostName={LIVE_HUB}.azure-devices.net;DeviceId={device_ids[1]};x509=true" + ) + + for auth_phase in DATAPLANE_AUTH_TYPES: + primary_key_cstring = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity connection-string show -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ).get_output_in_json() + + target_key = symmetric_key_device["authentication"]["symmetricKey"][ + "primaryKey" + ] + target_sym_cstring = sym_cstring_pattern.replace("#", target_key) + + assert target_sym_cstring == primary_key_cstring["connectionString"] + + secondary_key_cstring = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity connection-string show -d {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG} --kt secondary", + auth_type=auth_phase, + ) + ).get_output_in_json() + + target_key = symmetric_key_device["authentication"]["symmetricKey"][ + "secondaryKey" + ] + target_sym_cstring = sym_cstring_pattern.replace("#", target_key) + + assert target_sym_cstring == secondary_key_cstring["connectionString"] + + x509_cstring = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity connection-string show -d {device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ).get_output_in_json() + + assert cer_cstring_pattern == x509_cstring["connectionString"] + + # TODO: Improve validation of tests via micro device client or other means. + def test_iothub_device_generate_sas_token(self): + device_count = 2 + device_ids = self.generate_device_names(device_count) + + # Create SAS-auth device + self.cmd( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ) + + # Create non SAS-auth device + self.cmd( + f"iot hub device-identity create -d {device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG} --auth-method X509_ca" + ) + + for auth_phase in DATAPLANE_AUTH_TYPES: + self.cmd( + self.set_cmd_auth_type( + f"iot hub generate-sas-token -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[self.exists("sas")], + ) + + # Custom duration + self.cmd( + self.set_cmd_auth_type( + f"iot hub generate-sas-token -d {device_ids[0]} --du 1000 -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[self.exists("sas")], + ) + + # Custom key type + self.cmd( + self.set_cmd_auth_type( + f'iot hub generate-sas-token -d {device_ids[0]} --kt "secondary" -n {LIVE_HUB} -g {LIVE_RG}', + auth_type=auth_phase, + ), + checks=[self.exists("sas")], + ) + + # Error - generate sas token against non SAS device + self.cmd( + f"iot hub generate-sas-token -d {device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG}", + expect_failure=True, + ) + + # Mixed case connection string + cstring = self.connection_string + mixed_case_cstring = cstring.replace("HostName", "hostname", 1) + self.cmd( + f"iot hub generate-sas-token -d {device_ids[0]} --login {mixed_case_cstring}", + checks=[self.exists("sas")], + ) diff --git a/azext_iot/tests/iothub/devices/test_iothub_nested_edge_int.py b/azext_iot/tests/iothub/devices/test_iothub_nested_edge_int.py new file mode 100644 index 000000000..6d7da00ac --- /dev/null +++ b/azext_iot/tests/iothub/devices/test_iothub_nested_edge_int.py @@ -0,0 +1,265 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.tests.iothub import DATAPLANE_AUTH_TYPES + +settings = DynamoSettings(req_env_set=ENV_SET_TEST_IOTHUB_BASIC) + +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg + +# TODO: assert device scope format in device twin. +# from azext_iot.constants import DEVICE_DEVICESCOPE_PREFIX + + +class TestIoTHubNestedEdge(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTHubNestedEdge, self).__init__(test_case, LIVE_HUB, LIVE_RG) + + def test_iothub_nested_edge(self): + for auth_phase in DATAPLANE_AUTH_TYPES: + device_count = 3 + device_ids = self.generate_device_names(device_count) + edge_device_count = 2 + edge_device_ids = self.generate_device_names(edge_device_count) + + for edge_device_id in edge_device_ids: + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {edge_device_id} -n {LIVE_HUB} -g {LIVE_RG} --ee", + auth_type=auth_phase, + ), + checks=[ + self.check("capabilities.iotEdge", True), + self.exists("deviceScope"), + ], + ) + + for device_id in device_ids: + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {device_id} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[ + self.check("capabilities.iotEdge", False), + self.check("deviceScope", None), + ], + ) + + # Error - Get parent of edge device with no initial parent + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity parent show -d {edge_device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Error - Get parent of device which does not have any parent set + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity parent show -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Error - Set non-edge device as a parent of a non-edge device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity parent set -d {device_ids[0]} --pd {device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Set edge device as a parent of an edge device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity parent set -d {edge_device_ids[0]} --pd {edge_device_ids[1]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=self.is_empty(), + ) + + # Error - Add device as a child of a non-edge device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children add -d {device_ids[0]} --child-list {device_ids[1]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Add a space separated list of devices as children of an edge device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children add -d {edge_device_ids[0]} --child-list {' '.join(device_ids)} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=self.is_empty(), + ) + + # Error - setting edge device as a parent of non-edge device which already having different parent device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity parent set -d {device_ids[2]} --pd {edge_device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Setting edge device as a parent of non-edge device which already having different parent device by force + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity parent set -d {device_ids[2]} --pd {edge_device_ids[1]} " + f"-n {LIVE_HUB} -g {LIVE_RG} --force", + auth_type=auth_phase, + ), + checks=self.is_empty(), + ) + + # Get parent of device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity parent show -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", edge_device_ids[0]), + self.exists("deviceScope"), + ], + ) + + # Error - add same device as a child of same parent device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children add -d {edge_device_ids[0]} --child-list {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Error - add same device as a child of another edge device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children add -d {edge_device_ids[1]} --child-list {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Add same device as a child of another edge device by force + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children add -d {edge_device_ids[1]} --child-list {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG} --force", + auth_type=auth_phase, + ), + checks=self.is_empty(), + ) + + # List child devices of edge device + output = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children list -d {edge_device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ) + assert output.get_output_in_json() == [device_ids[1]] + + # Error - Remove all child devices of non-edge device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children remove -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --remove-all", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Remove all child devices from edge device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children remove -d {edge_device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG} --remove-all", + auth_type=auth_phase, + ), + checks=self.is_empty(), + ) + + # Error - remove all child devices of edge device which does not have any child devices + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children remove -d {edge_device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG} --remove-all", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Error - remove child device of edge device neither passing child devices list nor remove-all parameter + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children remove -d {edge_device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Error - remove edge device from edge device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children remove -d {edge_device_ids[1]} --child-list {edge_device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Error - remove device from edge device but device is a child of another edge device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children remove -d {edge_device_ids[1]} --child-list {device_ids[1]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Remove device + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children remove -d {edge_device_ids[0]} --child-list {device_ids[1]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=self.is_empty(), + ) + + # Error - remove device which does not have any parent set + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children remove -d {edge_device_ids[0]} --child-list {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # List child devices of edge device which doesn't have any children + output = self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity children list -d {edge_device_ids[1]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ) + assert output.get_output_in_json() == [] diff --git a/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py b/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py index 902adcfd9..fd44943f2 100644 --- a/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py +++ b/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta from azext_iot.tests import IoTLiveScenarioTest from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC - +from azext_iot.tests.iothub import DATAPLANE_AUTH_TYPES settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) LIVE_HUB = settings.env.azext_iot_testhub @@ -19,216 +19,228 @@ class TestIoTHubJobs(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTHubJobs, self).__init__(test_case, LIVE_HUB, LIVE_RG) - job_count = 3 - self.job_ids = self.generate_job_names(job_count) - def test_jobs(self): - device_count = 2 - device_ids_twin_tags = self.generate_device_names(device_count) - device_ids_twin_props = self.generate_device_names(device_count) + for auth_phase in DATAPLANE_AUTH_TYPES: + device_count = 2 + device_ids_twin_tags = self.generate_device_names(device_count) + device_ids_twin_props = self.generate_device_names(device_count) + + job_count = 3 + self.job_ids = self.generate_job_names(job_count) + + for device_id in device_ids_twin_tags + device_ids_twin_props: + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {device_id} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase + ) + ) + + # Focus is on scheduleUpdateTwin jobs until we improve JIT device simulation + + # Update twin tags scenario + self.kwargs[ + "twin_patch_tags" + ] = '{"tags": {"deviceClass": "Class1, Class2, Class3"}}' + query_condition = "deviceId in ['{}']".format("','".join(device_ids_twin_tags)) - for device_id in device_ids_twin_tags + device_ids_twin_props: self.cmd( - "iot hub device-identity create -d {} -n {} -g {}".format( - device_id, LIVE_HUB, LIVE_RG + self.set_cmd_auth_type( + f"iot hub job create --job-id {self.job_ids[0]} --job-type scheduleUpdateTwin -q \"{query_condition}\" " + f"-n {LIVE_HUB} -g {LIVE_RG} " + "--twin-patch '{twin_patch_tags}' --ttl 300 --wait", + auth_type=auth_phase + ), + checks=[ + self.check("jobId", self.job_ids[0]), + self.check("queryCondition", query_condition), + self.check("status", "completed"), + self.check("updateTwin.etag", "*"), + self.check( + "updateTwin.tags", + json.loads(self.kwargs["twin_patch_tags"])["tags"], + ), + self.check("type", "scheduleUpdateTwin"), + ], + ) + + for device_id in device_ids_twin_tags: + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-twin show -d {device_id} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase + ), + checks=[ + self.check( + "tags", json.loads(self.kwargs["twin_patch_tags"])["tags"] + ) + ], ) + + # Update twin desired properties + self.kwargs[ + "twin_patch_props" + ] = '{"properties": {"desired": {"arbitrary": "value"}}}' + query_condition = "deviceId in ['{}']".format("','".join(device_ids_twin_props)) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub job create --job-id {self.job_ids[1]} --job-type scheduleUpdateTwin -q \"{query_condition}\" " + f"-n {LIVE_HUB} -g {LIVE_RG} " + "--twin-patch '{twin_patch_props}' --ttl 300 --wait", + auth_type=auth_phase, + ), + checks=[ + self.check("jobId", self.job_ids[1]), + self.check("queryCondition", query_condition), + self.check("status", "completed"), + self.check("updateTwin.etag", "*"), + self.check( + "updateTwin.properties", + json.loads(self.kwargs["twin_patch_props"])["properties"], + ), + self.check("type", "scheduleUpdateTwin"), + ], ) - # Focus is on scheduleUpdateTwin jobs until we improve JIT device simulation - - # Update twin tags scenario - self.kwargs[ - "twin_patch_tags" - ] = '{"tags": {"deviceClass": "Class1, Class2, Class3"}}' - query_condition = "deviceId in ['{}']".format("','".join(device_ids_twin_tags)) - - self.cmd( - "iot hub job create --job-id {} --job-type {} -q \"{}\" --twin-patch '{}' -n {} -g {} --ttl 300 --wait".format( - self.job_ids[0], - "scheduleUpdateTwin", - query_condition, - "{twin_patch_tags}", - LIVE_HUB, - LIVE_RG, - ), - checks=[ - self.check("jobId", self.job_ids[0]), - self.check("queryCondition", query_condition), - self.check("status", "completed"), - self.check("updateTwin.etag", "*"), - self.check( - "updateTwin.tags", - json.loads(self.kwargs["twin_patch_tags"])["tags"], + # Error - omit queryCondition when scheduleUpdateTwin or scheduleDeviceMethod + self.cmd( + self.set_cmd_auth_type( + "iot hub job create --job-id {} --job-type {} --twin-patch '{}' -n {}".format( + self.job_ids[1], "scheduleUpdateTwin", "{twin_patch_props}", LIVE_HUB + ), + auth_type=auth_phase, ), - self.check("type", "scheduleUpdateTwin"), - ], - ) + expect_failure=True, + ) - for device_id in device_ids_twin_tags: self.cmd( - "iot hub device-twin show -d {} -n {} -g {}".format( - device_id, LIVE_HUB, LIVE_RG + self.set_cmd_auth_type( + "iot hub job create --job-id {} --job-type {} --twin-patch '{}' -n {}".format( + self.job_ids[1], "scheduleDeviceMethod", "{twin_patch_props}", LIVE_HUB + ), + auth_type=auth_phase + ), + expect_failure=True, + ) + + # Error - omit twin patch when scheduleUpdateTwin + self.cmd( + self.set_cmd_auth_type( + "iot hub job create --job-id {} --job-type {} -q '*' -n {}".format( + self.job_ids[1], "scheduleUpdateTwin", LIVE_HUB + ), + auth_type=auth_phase + ), + expect_failure=True, + ) + + # Error - omit method name when scheduleDeviceMethod + self.cmd( + self.set_cmd_auth_type( + "iot hub job create --job-id {} --job-type {} -q '*' -n {}".format( + self.job_ids[1], "scheduleDeviceMethod", LIVE_HUB + ), + auth_type=auth_phase + ), + expect_failure=True, + ) + + # Show Job tests + # Using --wait when creating effectively uses show + self.cmd( + self.set_cmd_auth_type( + "iot hub job show --job-id {} -n {} -g {}".format( + self.job_ids[0], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase ), checks=[ - self.check( - "tags", json.loads(self.kwargs["twin_patch_tags"])["tags"] - ) + self.check("jobId", self.job_ids[0]), + self.check("type", "scheduleUpdateTwin"), + ], + ) + + # Error - Show non-existant job + self.cmd( + self.set_cmd_auth_type( + "iot hub job show --job-id notarealjobid -n {} -g {}".format( + LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase + ), + expect_failure=True, + ) + + # Cancel Job test + # Create job to be cancelled - scheduled +7 days from now. + scheduled_time_iso = (datetime.utcnow() + timedelta(days=6)).isoformat() + + self.cmd( + self.set_cmd_auth_type( + "iot hub job create --job-id {} --job-type {} -q \"{}\" --twin-patch '{}' --start '{}' -n {} -g {}".format( + self.job_ids[2], + "scheduleUpdateTwin", + query_condition, + "{twin_patch_tags}", + scheduled_time_iso, + LIVE_HUB, + LIVE_RG, + ), + auth_type=auth_phase + ), + checks=[self.check("jobId", self.job_ids[2])], + ) + + # Allow time for job to transfer to scheduled state (cannot cancel job in running state) + from time import sleep + sleep(5) + + self.cmd( + self.set_cmd_auth_type( + "iot hub job show --job-id {} -n {} -g {}".format( + self.job_ids[2], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase + ), + checks=[ + self.check("jobId", self.job_ids[2]), + self.check("status", "scheduled"), ], ) - # Update twin desired properties, using connection string - self.kwargs[ - "twin_patch_props" - ] = '{"properties": {"desired": {"arbitrary": "value"}}}' - query_condition = "deviceId in ['{}']".format("','".join(device_ids_twin_props)) - - self.cmd( - "iot hub job create --job-id {} --job-type {} -q \"{}\" --twin-patch '{}' --login '{}' --ttl 300 --wait".format( - self.job_ids[1], - "scheduleUpdateTwin", - query_condition, - "{twin_patch_props}", - self.connection_string, - ), - checks=[ - self.check("jobId", self.job_ids[1]), - self.check("queryCondition", query_condition), - self.check("status", "completed"), - self.check("updateTwin.etag", "*"), - self.check( - "updateTwin.properties", - json.loads(self.kwargs["twin_patch_props"])["properties"], + # Cancel job + self.cmd( + self.set_cmd_auth_type( + "iot hub job cancel --job-id {} -n {} -g {}".format( + self.job_ids[2], LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase, ), - self.check("type", "scheduleUpdateTwin"), - ], - ) - - # Error - omit queryCondition when scheduleUpdateTwin or scheduleDeviceMethod - self.cmd( - "iot hub job create --job-id {} --job-type {} --twin-patch '{}' -n {}".format( - self.job_ids[1], "scheduleUpdateTwin", "{twin_patch_props}", LIVE_HUB - ), - expect_failure=True, - ) - - self.cmd( - "iot hub job create --job-id {} --job-type {} --twin-patch '{}' -n {}".format( - self.job_ids[1], "scheduleDeviceMethod", "{twin_patch_props}", LIVE_HUB - ), - expect_failure=True, - ) - - # Error - omit twin patch when scheduleUpdateTwin - self.cmd( - "iot hub job create --job-id {} --job-type {} -q '*' -n {}".format( - self.job_ids[1], "scheduleUpdateTwin", LIVE_HUB - ), - expect_failure=True, - ) - - # Error - omit method name when scheduleDeviceMethod - self.cmd( - "iot hub job create --job-id {} --job-type {} -q '*' -n {}".format( - self.job_ids[1], "scheduleDeviceMethod", LIVE_HUB - ), - expect_failure=True, - ) - - # Show Job tests - # Using --wait when creating effectively uses show - self.cmd( - "iot hub job show --job-id {} -n {} -g {}".format( - self.job_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("jobId", self.job_ids[0]), - self.check("type", "scheduleUpdateTwin"), - ], - ) - - # With connection string - self.cmd( - "iot hub job show --job-id {} --login {}".format( - self.job_ids[1], self.connection_string - ), - checks=[ - self.check("jobId", self.job_ids[1]), - self.check("type", "scheduleUpdateTwin"), - ], - ) - - # Error - Show non-existant job - self.cmd( - "iot hub job show --job-id notarealjobid -n {} -g {}".format( - LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # Cancel Job test - # Create job to be cancelled - scheduled +7 days from now. - scheduled_time_iso = (datetime.utcnow() + timedelta(days=6)).isoformat() - - self.cmd( - "iot hub job create --job-id {} --job-type {} -q \"{}\" --twin-patch '{}' --start '{}' -n {} -g {}".format( - self.job_ids[2], - "scheduleUpdateTwin", - query_condition, - "{twin_patch_tags}", - scheduled_time_iso, - LIVE_HUB, - LIVE_RG, - ), - checks=[self.check("jobId", self.job_ids[2])], - ) - - # Allow time for job to transfer to scheduled state (cannot cancel job in running state) - from time import sleep - sleep(5) - - self.cmd( - "iot hub job show --job-id {} -n {} -g {}".format( - self.job_ids[2], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("jobId", self.job_ids[2]), - self.check("status", "scheduled"), - ], - ) - - # Cancel job - self.cmd( - "iot hub job cancel --job-id {} -n {} -g {}".format( - self.job_ids[2], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("jobId", self.job_ids[2]), - self.check("status", "cancelled"), - ], - ) - - # Error - Cancel non-existant job - self.cmd( - "iot hub job cancel --job-id notarealjobid -n {} -g {}".format( - LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # List Job tests - # You can't explictly delete a job/job history so check for existance - job_result_set = self.cmd( - "iot hub job list -n {} -g {}".format(LIVE_HUB, LIVE_RG) - ).get_output_in_json() - - self.validate_job_list(jobs_set=job_result_set) - - # List Jobs - with connection string - job_result_set_cs = self.cmd( - "iot hub job list --login {}".format(self.connection_string) - ).get_output_in_json() - - self.validate_job_list(jobs_set=job_result_set_cs) + checks=[ + self.check("jobId", self.job_ids[2]), + self.check("status", "cancelled"), + ], + ) + + # Error - Cancel non-existant job + self.cmd( + self.set_cmd_auth_type( + "iot hub job cancel --job-id notarealjobid -n {} -g {}".format( + LIVE_HUB, LIVE_RG + ), + auth_type=auth_phase + ), + expect_failure=True, + ) + + # List Job tests + # You can't explictly delete a job/job history so check for existance + job_result_set = self.cmd( + "iot hub job list -n {} -g {}".format(LIVE_HUB, LIVE_RG) + ).get_output_in_json() + + self.validate_job_list(jobs_set=job_result_set) def validate_job_list(self, jobs_set): filtered_job_ids_result = {} diff --git a/azext_iot/tests/iothub/messaging/__init__.py b/azext_iot/tests/iothub/messaging/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/tests/iothub/messaging/__init__.py @@ -0,0 +1,5 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/azext_iot/tests/iothub/messaging/test_iothub_c2d_messages_int.py b/azext_iot/tests/iothub/messaging/test_iothub_c2d_messages_int.py new file mode 100644 index 000000000..a2329ec39 --- /dev/null +++ b/azext_iot/tests/iothub/messaging/test_iothub_c2d_messages_int.py @@ -0,0 +1,80 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json + +from uuid import uuid4 +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.common.shared import AuthenticationTypeDataplane +from azext_iot.tests.iothub import DATAPLANE_AUTH_TYPES +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.common.utility import ( + calculate_millisec_since_unix_epoch_utc, + validate_key_value_pairs +) + +settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg + + +class TestIoTHubC2DMessages(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTHubC2DMessages, self).__init__( + test_case, LIVE_HUB, LIVE_RG + ) + + def test_iothub_c2d_messages(self): + device_count = 1 + device_ids = self.generate_device_names(device_count) + + self.cmd( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ) + + for auth_phase in DATAPLANE_AUTH_TYPES: + test_ce = "utf-16" if auth_phase == AuthenticationTypeDataplane.login.value else "utf-8" + test_body = f"{uuid4()} шеллы 😁" # Mixed unicode blocks + test_props = f"key0={str(uuid4())};key1={str(uuid4())}" + test_cid = str(uuid4()) + test_mid = str(uuid4()) + test_ct = "text/plain" + test_et = calculate_millisec_since_unix_epoch_utc(3600) # milliseconds since epoch + + self.kwargs["c2d_json_send_data"] = json.dumps({"data": str(uuid4())}) + + # Send C2D message + self.cmd( + self.set_cmd_auth_type( + f"iot device c2d-message send -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --data '{test_body}' " + f"--cid {test_cid} --mid {test_mid} --ct {test_ct} --expiry {test_et} --ce {test_ce} -p '{test_props}'", + auth_type=auth_phase + ), + checks=self.is_empty(), + ) + + c2d_receive_result = self.cmd( + f"iot device c2d-message receive -d {device_ids[0]} --hub-name {LIVE_HUB} -g {LIVE_RG} --complete", + ).get_output_in_json() + + assert c2d_receive_result["data"] == test_body + + # Assert system properties + received_system_props = c2d_receive_result["properties"]["system"] + assert received_system_props["ContentEncoding"] == test_ce + assert received_system_props["ContentType"] == test_ct + assert received_system_props["iothub-correlationid"] == test_cid + assert received_system_props["iothub-messageid"] == test_mid + assert received_system_props["iothub-expiry"] + assert received_system_props["iothub-to"] == f"/devices/{device_ids[0]}/messages/devicebound" + + # Ack is tested in message feedback tests + assert received_system_props["iothub-ack"] == "none" + + # Assert app properties + received_app_props = c2d_receive_result["properties"]["app"] + assert received_app_props == validate_key_value_pairs(test_props) + assert c2d_receive_result["etag"] diff --git a/azext_iot/tests/iothub/modules/__init__.py b/azext_iot/tests/iothub/modules/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/tests/iothub/modules/__init__.py @@ -0,0 +1,5 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/azext_iot/tests/iothub/modules/test_iothub_module_twin_int.py b/azext_iot/tests/iothub/modules/test_iothub_module_twin_int.py new file mode 100644 index 000000000..30e6aea71 --- /dev/null +++ b/azext_iot/tests/iothub/modules/test_iothub_module_twin_int.py @@ -0,0 +1,229 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +import os +from pathlib import Path + +from azext_iot.common.utility import read_file_content +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.tests.generators import generate_generic_id +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.tests.iothub import DATAPLANE_AUTH_TYPES + +settings = DynamoSettings(req_env_set=ENV_SET_TEST_IOTHUB_BASIC) + +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg +CWD = os.path.dirname(os.path.abspath(__file__)) + + +class TestIoTHubModuleTwin(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTHubModuleTwin, self).__init__(test_case, LIVE_HUB, LIVE_RG) + + def test_iothub_module_twin(self): + for auth_phase in DATAPLANE_AUTH_TYPES: + device_count = 1 + device_ids = self.generate_device_names(device_count) + module_count = 1 + module_ids = self.generate_device_names(module_count) + + patch_desired = { + generate_generic_id(): generate_generic_id(), + generate_generic_id(): generate_generic_id(), + } + patch_tags = { + generate_generic_id(): generate_generic_id(), + generate_generic_id(): generate_generic_id(), + } + + self.kwargs["patch_desired"] = json.dumps(patch_desired) + self.kwargs["patch_tags"] = json.dumps(patch_tags) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity create -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ) + + # Initial twin state + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-twin show -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[ + self.check("moduleId", module_ids[0]), + self.check("deviceId", device_ids[0]), + self.exists("properties.desired"), + self.exists("properties.reported"), + ], + ).get_output_in_json() + + assert d0_twin["properties"]["desired"]["$version"] == 1 + assert d0_twin["properties"]["reported"]["$version"] == 1 + + # Patch based twin update of desired props + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-twin update -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--desired '{patch_desired}'", # Not f-string due to CLI TestFramework self.kwargs application :( + auth_type=auth_phase, + ) + ).get_output_in_json() + + assert d0_twin["properties"]["desired"]["$version"] == 2 + + for key in patch_desired: + assert d0_twin["properties"]["desired"][key] == patch_desired[key] + + # Patch based twin update of tag props + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-twin update -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--tags '{patch_tags}'", # Not f-string due to CLI TestFramework self.kwargs application :( + auth_type=auth_phase, + ) + ).get_output_in_json() + + for key in patch_tags: + assert d0_twin["tags"][key] == patch_tags[key] + + for key in patch_desired: + assert d0_twin["properties"]["desired"][key] == patch_desired[key] + + assert d0_twin["properties"]["desired"]["$version"] == 2 + + # Patch based twin update of tag and desired props + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-twin update -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--tags '{patch_tags}' --desired '{patch_desired}'", + auth_type=auth_phase, + ) + ).get_output_in_json() + + for key in patch_tags: + assert d0_twin["tags"][key] == patch_tags[key] + + for key in patch_desired: + assert d0_twin["properties"]["desired"][key] == patch_desired[key] + + assert d0_twin["properties"]["desired"]["$version"] == 3 + + # Prepare removal of all twin tag properties + for key in patch_tags: + patch_tags[key] = None + self.kwargs["patch_tags"] = json.dumps(patch_tags) + + # Remove all twin tag properties + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-twin update -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--tags '{patch_tags}'", + auth_type=auth_phase, + ) + ).get_output_in_json() + assert d0_twin["tags"] is None + + # Prepare removal of single desired twin property + target_key = list(patch_desired.keys())[0] + patch_desired[target_key] = None + self.kwargs["patch_desired"] = json.dumps(patch_desired) + + # Remove single desired property + d0_twin = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-twin update -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--desired '{patch_desired}'", + auth_type=auth_phase, + ) + ).get_output_in_json() + + assert d0_twin["properties"]["desired"].get(target_key) is None + assert d0_twin["properties"]["desired"]["$version"] == 4 + + # Validation error --desired is not an object + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-twin update -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--desired 'badinput'", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + def test_iothub_module_twin_replace(self): + for auth_phase in DATAPLANE_AUTH_TYPES: + device_count = 1 + device_ids = self.generate_device_names(device_count) + module_count = 1 + module_ids = self.generate_device_names(module_count) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity create -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ) + + replace_twin_content_path = os.path.join( + Path(CWD).parent, "test_generic_replace.json" + ) + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-twin replace -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} -j " + f"'{replace_twin_content_path}'", + auth_type=auth_phase, + ), + checks=[ + self.check("moduleId", module_ids[0]), + self.check("deviceId", device_ids[0]), + self.check("properties.desired.awesome", 9001), + self.check("properties.desired.temperature.min", 10), + self.check("properties.desired.temperature.max", 100), + self.check("tags.location.region", "US"), + ], + ) + + # Inline json + replace_twin_content_path = os.path.join( + Path(CWD).parent, "test_generic_replace.json" + ) + self.kwargs["inline_replace_content"] = read_file_content( + replace_twin_content_path + ) + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-twin replace -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "-j '{inline_replace_content}'", + auth_type=auth_phase, + ), + checks=[ + self.check("moduleId", module_ids[0]), + self.check("deviceId", device_ids[0]), + self.check("properties.desired.awesome", 9001), + self.check("properties.desired.temperature.min", 10), + self.check("properties.desired.temperature.max", 100), + self.check("tags.location.region", "US"), + ], + ) diff --git a/azext_iot/tests/iothub/modules/test_iothub_modules_int.py b/azext_iot/tests/iothub/modules/test_iothub_modules_int.py new file mode 100644 index 000000000..cded3466b --- /dev/null +++ b/azext_iot/tests/iothub/modules/test_iothub_modules_int.py @@ -0,0 +1,396 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.tests.iothub import ( + DATAPLANE_AUTH_TYPES, + PRIMARY_THUMBPRINT, + SECONDARY_THUMBPRINT, + DEVICE_TYPES, +) + +settings = DynamoSettings(req_env_set=ENV_SET_TEST_IOTHUB_BASIC) + +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg + + +class TestIoTHubModules(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTHubModules, self).__init__(test_case, LIVE_HUB, LIVE_RG) + + def test_iothub_module_identity(self): + for auth_phase in DATAPLANE_AUTH_TYPES: + for device_type in DEVICE_TYPES: + device_count = 1 + module_count = 4 + device_ids = self.generate_device_names( + device_count, edge=device_type == "edge" + ) + module_ids = self.generate_module_names(module_count) + edge_enabled = "--edge-enabled" if device_type == "edge" else "" + + # Symmetric key device creation + self.cmd( + self.set_cmd_auth_type( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} {edge_enabled}", + auth_type=auth_phase, + ), + ) + + m0_d0_checks = [ + self.check("deviceId", device_ids[0]), + self.check("moduleId", module_ids[0]), + self.exists("authentication.symmetricKey.primaryKey"), + self.exists("authentication.symmetricKey.secondaryKey"), + ] + + # Create module identity with symmetric keys + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity create --module-id {module_ids[0]} --device-id {device_ids[0]} " + f"--hub-name {LIVE_HUB} --resource-group {LIVE_RG}", + auth_type=auth_phase, + ), + checks=m0_d0_checks, + ) + + # Create module identity with x509 thumbprint + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity create -m {module_ids[1]} -d {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG} --auth-method x509_thumbprint --primary-thumbprint {PRIMARY_THUMBPRINT} " + f"--secondary-thumbprint {SECONDARY_THUMBPRINT}", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", device_ids[0]), + self.check("moduleId", module_ids[1]), + self.check("connectionState", "Disconnected"), + self.check("authentication.symmetricKey.primaryKey", None), + self.check("authentication.symmetricKey.secondaryKey", None), + self.check( + "authentication.x509Thumbprint.primaryThumbprint", + PRIMARY_THUMBPRINT, + ), + self.check( + "authentication.x509Thumbprint.secondaryThumbprint", + SECONDARY_THUMBPRINT, + ), + ], + ) + + # Create module identity with generated x509 thumbprint + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity create -m {module_ids[2]} -d {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG} --am x509_thumbprint --valid-days 1", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", device_ids[0]), + self.check("moduleId", module_ids[2]), + self.check("connectionState", "Disconnected"), + self.check("authentication.symmetricKey.primaryKey", None), + self.check("authentication.symmetricKey.secondaryKey", None), + self.exists("authentication.x509Thumbprint.primaryThumbprint"), + self.check( + "authentication.x509Thumbprint.secondaryThumbprint", None + ), + ], + ) + + # Create module identity with x509 ca + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity create -m {module_ids[3]} -d {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG} --am x509_ca", + auth_type=auth_phase, + ), + checks=[ + self.check("deviceId", device_ids[0]), + self.check("moduleId", module_ids[3]), + self.check("connectionState", "Disconnected"), + self.check("authentication.symmetricKey.primaryKey", None), + self.check("authentication.symmetricKey.secondaryKey", None), + self.check( + "authentication.x509Thumbprint.primaryThumbprint", None + ), + self.check( + "authentication.x509Thumbprint.secondaryThumbprint", None + ), + ], + ) + + # Show symmetric key module identity + m0_d0_show = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity show -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=m0_d0_checks, + ).get_output_in_json() + + # Reset module symmetric key using module-identity generic update + m0_d0_updated = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity update -m {module_ids[0]} -d {device_ids[0]} " + f'-n {LIVE_HUB} -g {LIVE_RG} --set authentication.symmetricKey.primaryKey="" ' + 'authentication.symmetricKey.secondaryKey=""', + auth_type=auth_phase, + ) + ).get_output_in_json() + assert ( + m0_d0_updated["authentication"]["symmetricKey"]["primaryKey"] + != m0_d0_show["authentication"]["symmetricKey"]["primaryKey"] + ) + assert ( + m0_d0_updated["authentication"]["symmetricKey"]["secondaryKey"] + != m0_d0_show["authentication"]["symmetricKey"]["secondaryKey"] + ) + + query_checks = [] + for m in module_ids: + query_checks.append(self.exists(f"[?moduleId=='{m}']")) + if device_type == "edge": + query_checks.append(self.exists("[?moduleId=='$edgeAgent']")) + query_checks.append(self.exists("[?moduleId=='$edgeHub']")) + + # Query device modules. Edge devices include the $edgeAgent and $edgeHub system modules. + module_query_result = self.cmd( + self.set_cmd_auth_type( + f"iot hub query -n {LIVE_HUB} -g {LIVE_RG} " + f"-q \"select * from devices.modules where devices.deviceId='{device_ids[0]}'\"", + auth_type=auth_phase, + ), + checks=query_checks, + ).get_output_in_json() + + target_module_count = ( + 2 + module_count if device_type == "edge" else module_count + ) + assert len(module_query_result) == target_module_count + + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity list -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=query_checks, + ) + + # Delete module identity. + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity delete -m {module_ids[2]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=self.is_empty(), + ) + + # Validate deletion worked. + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity show -m {module_ids[2]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + def test_iothub_module_renew_key(self): + device_count = 1 + device_ids = self.generate_device_names(device_count) + module_count = 2 + module_ids = self.generate_device_names(module_count) + + self.cmd( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ).get_output_in_json() + + symmetric_key_module = self.cmd( + f"iot hub module-identity create -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ).get_output_in_json() + + self.cmd( + f"iot hub module-identity create -m {module_ids[1]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --am x509_ca" + ) + + for auth_phase in DATAPLANE_AUTH_TYPES: + renew_primary_key_module = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity renew-key -m {module_ids[0]} " + f"-d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --kt primary", + auth_type=auth_phase, + ) + ).get_output_in_json() + assert ( + renew_primary_key_module["authentication"]["symmetricKey"]["primaryKey"] + != symmetric_key_module["authentication"]["symmetricKey"]["primaryKey"] + ) + assert ( + renew_primary_key_module["authentication"]["symmetricKey"][ + "secondaryKey" + ] + == symmetric_key_module["authentication"]["symmetricKey"]["secondaryKey"] + ) + + swap_keys_module = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity renew-key -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --kt swap", + auth_type=auth_phase, + ) + ).get_output_in_json() + assert ( + renew_primary_key_module["authentication"]["symmetricKey"]["primaryKey"] + == swap_keys_module["authentication"]["symmetricKey"]["secondaryKey"] + ) + assert ( + renew_primary_key_module["authentication"]["symmetricKey"]["secondaryKey"] + == swap_keys_module["authentication"]["symmetricKey"]["primaryKey"] + ) + + self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity renew-key -m {module_ids[1]} " + f"-d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --kt secondary", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + def test_iothub_module_connection_string_show(self): + device_count = 1 + device_ids = self.generate_device_names(device_count) + module_count = 2 + module_ids = self.generate_device_names(module_count) + + self.cmd( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ).get_output_in_json() + + symmetric_key_module = self.cmd( + f"iot hub module-identity create -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ).get_output_in_json() + + self.cmd( + f"iot hub module-identity create -m {module_ids[1]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} --am x509_ca" + ) + + sym_cstring_pattern = ( + f"HostName={LIVE_HUB}.azure-devices.net;DeviceId={device_ids[0]};" + f"ModuleId={module_ids[0]};SharedAccessKey=#" + ) + cer_cstring_pattern = f"HostName={LIVE_HUB}.azure-devices.net;DeviceId={device_ids[0]};ModuleId={module_ids[1]};x509=true" + + for auth_phase in DATAPLANE_AUTH_TYPES: + primary_key_cstring = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity connection-string show -m {module_ids[0]} -d {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ).get_output_in_json() + + target_key = symmetric_key_module["authentication"]["symmetricKey"][ + "primaryKey" + ] + target_sym_cstring = sym_cstring_pattern.replace("#", target_key) + + assert target_sym_cstring == primary_key_cstring["connectionString"] + + secondary_key_cstring = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity connection-string show -m {module_ids[0]} -d {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG} --kt secondary", + auth_type=auth_phase, + ) + ).get_output_in_json() + + target_key = symmetric_key_module["authentication"]["symmetricKey"][ + "secondaryKey" + ] + target_sym_cstring = sym_cstring_pattern.replace("#", target_key) + + assert target_sym_cstring == secondary_key_cstring["connectionString"] + + x509_cstring = self.cmd( + self.set_cmd_auth_type( + f"iot hub module-identity connection-string show -m {module_ids[1]} -d {device_ids[0]} " + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ) + ).get_output_in_json() + + assert cer_cstring_pattern == x509_cstring["connectionString"] + + def test_iothub_module_generate_sas_token(self): + device_count = 1 + device_ids = self.generate_device_names(device_count) + + module_count = 2 + module_ids = self.generate_device_names(module_count) + + self.cmd( + f"iot hub device-identity create -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ) + + self.cmd( + f"iot hub module-identity create -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}" + ) + + self.cmd( + f"iot hub module-identity create -m {module_ids[1]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG} " + "--auth-method x509_ca" + ) + + for auth_phase in DATAPLANE_AUTH_TYPES: + self.cmd( + self.set_cmd_auth_type( + f"iot hub generate-sas-token -m {module_ids[0]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[self.exists("sas")], + ) + + # Custom duration + self.cmd( + self.set_cmd_auth_type( + f"iot hub generate-sas-token -m {module_ids[0]} -d {device_ids[0]} --du 1000 -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[self.exists("sas")], + ) + + # Custom key type + self.cmd( + self.set_cmd_auth_type( + f'iot hub generate-sas-token -m {module_ids[0]} -d {device_ids[0]} --kt "secondary" ' + f"-n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[self.exists("sas")], + ) + + # Error - generate sas token against non SAS module + self.cmd( + f"iot hub generate-sas-token -m {module_ids[1]} -d {device_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + expect_failure=True, + ) + + # Error - generate sas token against module with no device + self.cmd( + f"iot hub generate-sas-token -m {module_ids[0]} -n {LIVE_HUB} -g {LIVE_RG}", + expect_failure=True, + ) + + # Mixed case connection string + cstring = self.connection_string + mixed_case_cstring = cstring.replace("HostName", "hostname", 1) + self.cmd( + f"iot hub generate-sas-token -m {module_ids[0]} -d {device_ids[0]} --login {mixed_case_cstring}", + checks=[self.exists("sas")], + ) diff --git a/azext_iot/tests/iothub/test_iot_ext_int.py b/azext_iot/tests/iothub/test_iot_ext_int.py index 81b67f0f5..9b88cea0c 100644 --- a/azext_iot/tests/iothub/test_iot_ext_int.py +++ b/azext_iot/tests/iothub/test_iot_ext_int.py @@ -6,1412 +6,69 @@ import os import pytest -import warnings +from time import sleep -from azext_iot.common.utility import read_file_content from azext_iot.tests import IoTLiveScenarioTest from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC -from azext_iot.constants import DEVICE_DEVICESCOPE_PREFIX +from azext_iot.common.utility import ensure_iothub_sdk_min_version -opt_env_set = ["azext_iot_teststorageuri", "azext_iot_identity_teststorageid"] - -settings = DynamoSettings( - req_env_set=ENV_SET_TEST_IOTHUB_BASIC, opt_env_set=opt_env_set -) - -LIVE_HUB = settings.env.azext_iot_testhub -LIVE_RG = settings.env.azext_iot_testrg - -# Set this environment variable to your empty blob container sas uri to test device export and enable file upload test. -# For file upload, you will need to have configured your IoT Hub before running. -LIVE_STORAGE = settings.env.azext_iot_teststorageuri - -# Set this environment variable to enable identity-based integration tests -# You will need permissions to add and remove role assignments for this storage account -LIVE_STORAGE_ID = settings.env.azext_iot_identity_teststorageid - -LIVE_CONSUMER_GROUPS = ["test1", "test2", "test3"] - -CWD = os.path.dirname(os.path.abspath(__file__)) - -PRIMARY_THUMBPRINT = "A361EA6A7119A8B0B7BBFFA2EAFDAD1F9D5BED8C" -SECONDARY_THUMBPRINT = "14963E8F3BA5B3984110B3C1CA8E8B8988599087" - - -class TestIoTHub(IoTLiveScenarioTest): - def __init__(self, test_case): - super(TestIoTHub, self).__init__(test_case, LIVE_HUB, LIVE_RG) - - def test_hub(self): - self.cmd( - "az iot hub generate-sas-token -n {} -g {}".format(LIVE_HUB, LIVE_RG), - checks=[self.exists("sas")], - ) - - self.cmd( - "az iot hub generate-sas-token -n {}".format(LIVE_HUB), - checks=[self.exists("sas")], - ) - - self.cmd( - "az iot hub generate-sas-token -n {} --du {}".format(LIVE_HUB, "1000"), - checks=[self.exists("sas")], - ) - - # With connection string - self.cmd( - "az iot hub generate-sas-token --login {}".format(self.connection_string), - checks=[self.exists("sas")], - ) - - self.cmd( - "az iot hub generate-sas-token --login {} --pn somepolicy".format( - self.connection_string - ), - expect_failure=True, - ) - - # Test 'az iot hub connection-string show' - conn_str_pattern = r'^HostName={0}.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey='.format( - LIVE_HUB) - conn_str_eventhub_pattern = (r'^Endpoint=sb://(.+?)servicebus.windows.net/;SharedAccessKeyName=' - r'iothubowner;SharedAccessKey=(.+?);EntityPath=') - defaultpolicy = "iothubowner" - nonexistantpolicy = "badpolicy" - - hubs_in_sub = self.cmd('iot hub connection-string show').get_output_in_json() - hubs_in_rg = self.cmd('iot hub connection-string show -g {}'.format(LIVE_RG)).get_output_in_json() - assert len(hubs_in_sub) >= len(hubs_in_rg) - - self.cmd('iot hub connection-string show -n {0}'.format(LIVE_HUB), checks=[ - self.check_pattern('connectionString', conn_str_pattern) - ]) - - self.cmd('iot hub connection-string show -n {0} --pn {1}'.format(LIVE_HUB, defaultpolicy), checks=[ - self.check_pattern('connectionString', conn_str_pattern) - ]) - - self.cmd( - 'iot hub connection-string show -n {0} --pn {1}'.format(LIVE_HUB, nonexistantpolicy), - expect_failure=True, - ) - - self.cmd( - 'iot hub connection-string show --pn {0}'.format(nonexistantpolicy), - checks=[self.check('length(@)', 0)] - ) - - self.cmd('iot hub connection-string show -n {0} --eh'.format(LIVE_HUB), checks=[ - self.check_pattern('connectionString', conn_str_eventhub_pattern) - ]) - - self.cmd('iot hub connection-string show -n {0} -g {1}'.format(LIVE_HUB, LIVE_RG), checks=[ - self.check('length(@)', 1), - self.check_pattern('connectionString', conn_str_pattern) - ]) - - self.cmd('iot hub connection-string show -n {0} -g {1} --all'.format(LIVE_HUB, LIVE_RG), checks=[ - self.greater_than('length(connectionString[*])', 0), - self.check_pattern('connectionString[0]', conn_str_pattern) - ]) - - self.cmd('iot hub connection-string show -n {0} -g {1} --all --eh'.format(LIVE_HUB, LIVE_RG), checks=[ - self.greater_than('length(connectionString[*])', 0), - self.check_pattern('connectionString[0]', conn_str_eventhub_pattern) - ]) - - # With connection string - # Error can't change key for a sas token with conn string - self.cmd( - "az iot hub generate-sas-token --login {} --kt secondary".format( - self.connection_string - ), - expect_failure=True, - ) - - self.cmd( - 'iot hub query --hub-name {} -q "{}"'.format( - LIVE_HUB, "select * from devices" - ), - checks=[self.check("length([*])", 0)], - ) - - # With connection string - self.cmd( - 'iot hub query --query-command "{}" --login {}'.format( - "select * from devices", self.connection_string - ), - checks=[self.check("length([*])", 0)], - ) - - # Test mode 2 handler - self.cmd( - 'iot hub query -q "{}"'.format("select * from devices"), expect_failure=True - ) - - self.cmd( - 'iot hub query -q "{}" -l "{}"'.format( - "select * from devices", "Hostname=badlogin;key=1235" - ), - expect_failure=True, - ) - - -class TestIoTHubDevices(IoTLiveScenarioTest): - def __init__(self, test_case): - super(TestIoTHubDevices, self).__init__( - test_case, LIVE_HUB, LIVE_RG - ) - - def test_hub_devices(self): - device_count = 5 - edge_device_count = 2 - edge_x509_device_count = 2 - total_edge_device_count = edge_x509_device_count + edge_device_count - - device_ids = self.generate_device_names(device_count) - edge_device_ids = self.generate_device_names(edge_device_count, edge=True) - edge_x509_device_ids = self.generate_device_names(edge_x509_device_count, edge=True) - - total_devices = device_count + total_edge_device_count - - self.cmd( - "iot hub device-identity create -d {} -n {} -g {}".format( - device_ids[4], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", device_ids[4]), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("connectionState", "Disconnected"), - self.check("capabilities.iotEdge", False), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - for edge_device in edge_device_ids: - self.cmd( - "iot hub device-identity create -d {} -n {} -g {} --ee --add-children {} --force".format( - edge_device, LIVE_HUB, LIVE_RG, device_ids[4] - ), - checks=[ - self.check("deviceId", edge_device), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("connectionState", "Disconnected"), - self.check("capabilities.iotEdge", True), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - device_scope_str_pattern = r"^{}{}-".format( - DEVICE_DEVICESCOPE_PREFIX, edge_device - ) - self.cmd( - "iot hub device-identity show -d {} -n {} -g {}".format( - device_ids[4], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", device_ids[4]), - self.check_pattern("deviceScope", device_scope_str_pattern), - ], - ) - - # All edge devices + child device - query_checks = [self.check("length([*])", total_edge_device_count + 1)] - for i in edge_device_ids: - query_checks.append(self.exists("[?deviceId==`{}`]".format(i))) - query_checks.append(self.exists("[?deviceId==`{}`]".format(device_ids[4]))) - - # Edge x509_thumbprint - self.cmd( - "iot hub device-identity create -d {} -n {} -g {} --auth-method x509_thumbprint --ptp {} --stp {} --ee".format( - edge_x509_device_ids[0], LIVE_HUB, LIVE_RG, PRIMARY_THUMBPRINT, SECONDARY_THUMBPRINT - ), - checks=[ - self.check("deviceId", edge_x509_device_ids[0]), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("capabilities.iotEdge", True), - self.check("connectionState", "Disconnected"), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.check( - "authentication.x509Thumbprint.primaryThumbprint", - PRIMARY_THUMBPRINT, - ), - self.check( - "authentication.x509Thumbprint.secondaryThumbprint", - SECONDARY_THUMBPRINT, - ), - ] - ) - - # Edge x509_ca - self.cmd( - "iot hub device-identity create -d {} -n {} -g {} --auth-method x509_ca --ee".format( - edge_x509_device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", edge_x509_device_ids[1]), - self.check("status", "enabled"), - self.check("capabilities.iotEdge", True), - self.check("connectionState", "Disconnected"), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.check("authentication.x509Thumbprint.primaryThumbprint", None), - self.check("authentication.x509Thumbprint.secondaryThumbprint", None), - self.check("authentication.type", "certificateAuthority") - ] - ) - - self.cmd( - 'iot hub query --hub-name {} -g {} -q "{}"'.format( - LIVE_HUB, LIVE_RG, "select * from devices" - ), - checks=query_checks, - ) - - # With connection string - self.cmd( - 'iot hub query -q "{}" --login {}'.format( - "select * from devices", self.connection_string - ), - checks=query_checks, - ) - - # -1 for no return limit - self.cmd( - 'iot hub query -q "{}" --login {} --top -1'.format( - "select * from devices", self.connection_string - ), - checks=query_checks, - ) - - self.cmd( - """iot hub device-identity create --device-id {} --hub-name {} --resource-group {} - --auth-method x509_thumbprint --primary-thumbprint {} --secondary-thumbprint {}""".format( - device_ids[0], - LIVE_HUB, - LIVE_RG, - PRIMARY_THUMBPRINT, - SECONDARY_THUMBPRINT, - ), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("capabilities.iotEdge", False), - self.check("connectionState", "Disconnected"), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.check( - "authentication.x509Thumbprint.primaryThumbprint", - PRIMARY_THUMBPRINT, - ), - self.check( - "authentication.x509Thumbprint.secondaryThumbprint", - SECONDARY_THUMBPRINT, - ), - ], - ) - - self.cmd( - """iot hub device-identity create --device-id {} --hub-name {} --resource-group {} - --auth-method x509_thumbprint --valid-days {}""".format( - device_ids[1], LIVE_HUB, LIVE_RG, 10 - ), - checks=[ - self.check("deviceId", device_ids[1]), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("capabilities.iotEdge", False), - self.check("connectionState", "Disconnected"), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.exists("authentication.x509Thumbprint.primaryThumbprint"), - self.check("authentication.x509Thumbprint.secondaryThumbprint", None), - ], - ) - - # With connection string - status_reason = "Test Status Reason" - self.cmd( - '''iot hub device-identity create --device-id {} --login {} - --auth-method x509_ca --status disabled --status-reason "{}"'''.format( - device_ids[2], self.connection_string, status_reason - ), - checks=[ - self.check("deviceId", device_ids[2]), - self.check("status", "disabled"), - self.check("statusReason", status_reason), - self.check("capabilities.iotEdge", False), - self.check("connectionState", "Disconnected"), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.check("authentication.x509Thumbprint.primaryThumbprint", None), - self.check("authentication.x509Thumbprint.secondaryThumbprint", None), - ], - ) - - child_device_scope_str_pattern = r"^{}{}-".format( - DEVICE_DEVICESCOPE_PREFIX, edge_device_ids[0] - ) - - # Create device with parent device - self.cmd( - """iot hub device-identity create --device-id {} --hub-name {} --resource-group {} - --auth-method x509_thumbprint --valid-days {} --set-parent {}""".format( - device_ids[3], LIVE_HUB, LIVE_RG, 10, edge_device_ids[0] - ), - checks=[ - self.check("deviceId", device_ids[3]), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("capabilities.iotEdge", False), - self.check("connectionState", "Disconnected"), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.exists("authentication.x509Thumbprint.primaryThumbprint"), - self.check("authentication.x509Thumbprint.secondaryThumbprint", None), - self.exists("deviceScope"), - self.exists("parentScopes"), - self.check_pattern("deviceScope", child_device_scope_str_pattern), - ], - ) - - self.cmd( - "iot hub device-identity show -d {} -n {} -g {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("connectionState", "Disconnected"), - self.check("capabilities.iotEdge", True), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - # With connection string - self.cmd( - "iot hub device-identity show -d {} --login {}".format( - edge_device_ids[0], self.connection_string - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("connectionState", "Disconnected"), - self.check("capabilities.iotEdge", True), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - # List all devices - self.cmd( - "iot hub device-identity list --hub-name {} --resource-group {}".format( - LIVE_HUB, LIVE_RG - ), - checks=[self.check("length([*])", total_devices)], - ) - - self.cmd( - "iot hub device-identity list --hub-name {} --resource-group {} --top -1".format( - LIVE_HUB, LIVE_RG - ), - checks=[self.check("length([*])", total_devices)], - ) - - # List only edge devices - self.cmd( - "iot hub device-identity list -n {} -g {} --ee".format(LIVE_HUB, LIVE_RG), - checks=[self.check("length([*])", total_edge_device_count)], - ) - - # With connection string - self.cmd( - "iot hub device-identity list --ee --login {}".format(self.connection_string), - checks=[self.check("length([*])", total_edge_device_count)], - ) - - self.cmd( - "iot hub device-identity update -d {} -n {} -g {} --set capabilities.iotEdge={}".format( - device_ids[0], LIVE_HUB, LIVE_RG, True - ), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("status", "enabled"), - self.check("capabilities.iotEdge", True), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.check( - "authentication.x509Thumbprint.primaryThumbprint", - PRIMARY_THUMBPRINT, - ), - self.check( - "authentication.x509Thumbprint.secondaryThumbprint", - SECONDARY_THUMBPRINT, - ), - ], - ) - - self.cmd( - "iot hub device-identity update -d {} -n {} -g {} --ee {} --auth-method {}" - .format(device_ids[0], LIVE_HUB, LIVE_RG, False, 'x509_ca'), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("status", "enabled"), - self.check("capabilities.iotEdge", False), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.check("authentication.x509Thumbprint.primaryThumbprint", None), - self.check("authentication.x509Thumbprint.secondaryThumbprint", None), - self.check("authentication.type", 'certificateAuthority') - ] - ) - - self.cmd( - "iot hub device-identity update -d {} -n {} -g {} --status-reason {}" - .format(device_ids[0], LIVE_HUB, LIVE_RG, 'TestStatusReason'), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("statusReason", 'TestStatusReason'), - ] - ) - - self.cmd( - "iot hub device-identity update -d {} -n {} -g {} --ee {} --status {}" - " --status-reason {} --auth-method {} --ptp {} --stp {}" - .format(device_ids[0], LIVE_HUB, LIVE_RG, False, 'enabled', - 'StatusReasonUpdated', 'x509_thumbprint', PRIMARY_THUMBPRINT, SECONDARY_THUMBPRINT), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("status", "enabled"), - self.check("capabilities.iotEdge", False), - self.check("statusReason", 'StatusReasonUpdated'), - self.check("authentication.x509Thumbprint.primaryThumbprint", PRIMARY_THUMBPRINT), - self.check("authentication.x509Thumbprint.secondaryThumbprint", SECONDARY_THUMBPRINT), - ] - ) - - self.cmd("iot hub device-identity update -d {} -n {} -g {} --auth-method {}" - .format(device_ids[0], LIVE_HUB, LIVE_RG, 'x509_thumbprint'), - expect_failure=True) - - self.cmd("iot hub device-identity update -d {} -n {} -g {} --auth-method {} --pk {}" - .format(device_ids[0], LIVE_HUB, LIVE_RG, 'shared_private_key', '123'), - expect_failure=True) - - self.cmd( - '''iot hub device-identity update -d {} -n {} -g {} --primary-key="" - --secondary-key=""'''.format( - device_ids[4], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", device_ids[4]), - self.check("status", "enabled"), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - # With connection string - self.cmd( - '''iot hub device-identity update -d {} --login {} --set authentication.symmetricKey.primaryKey="" - authentication.symmetricKey.secondaryKey=""'''.format( - device_ids[4], self.connection_string - ), - checks=[ - self.check("deviceId", device_ids[4]), - self.check("status", "enabled"), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - # Test 'az iot hub device renew-key' - device = self.cmd( - '''iot hub device-identity renew-key -d {} -n {} -g {} --kt primary - '''.format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", edge_device_ids[1]) - ] - ).get_output_in_json() - - # Test swap keys 'az iot hub device renew-key' - self.cmd( - '''iot hub device-identity renew-key -d {} -n {} -g {} --kt swap - '''.format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("authentication.symmetricKey.primaryKey", device['authentication']['symmetricKey']['secondaryKey']), - self.check("authentication.symmetricKey.secondaryKey", device['authentication']['symmetricKey']['primaryKey']) - ], - ) - - # Test 'az iot hub device renew-key' with non sas authentication - self.cmd("iot hub device-identity renew-key -d {} -n {} -g {} --kt secondary" - .format(device_ids[0], LIVE_HUB, LIVE_RG), - expect_failure=True) - - sym_conn_str_pattern = r"^HostName={}\.azure-devices\.net;DeviceId={};SharedAccessKey=".format( - LIVE_HUB, edge_device_ids[0] - ) - cer_conn_str_pattern = r"^HostName={}\.azure-devices\.net;DeviceId={};x509=true".format( - LIVE_HUB, device_ids[2] - ) - - self.cmd( - "iot hub device-identity show-connection-string -d {} -n {} -g {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[self.check_pattern("connectionString", sym_conn_str_pattern)], - ) - - self.cmd( - "iot hub device-identity show-connection-string -d {} -n {} -g {} --kt {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, "secondary" - ), - checks=[self.check_pattern("connectionString", sym_conn_str_pattern)], - ) - - self.cmd( - "iot hub device-identity show-connection-string -d {} -n {} -g {}".format( - device_ids[2], LIVE_HUB, LIVE_RG - ), - checks=[self.check_pattern("connectionString", cer_conn_str_pattern)], - ) - - self.cmd( - "iot hub device-identity connection-string show -d {} -n {} -g {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[self.check_pattern("connectionString", sym_conn_str_pattern)], - ) - - self.cmd( - "iot hub device-identity connection-string show -d {} -n {} -g {} --kt {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, "secondary" - ), - checks=[self.check_pattern("connectionString", sym_conn_str_pattern)], - ) - - self.cmd( - "iot hub device-identity connection-string show -d {} -n {} -g {}".format( - device_ids[2], LIVE_HUB, LIVE_RG - ), - checks=[self.check_pattern("connectionString", cer_conn_str_pattern)], - ) - - self.cmd( - "iot hub generate-sas-token -n {} -g {} -d {}".format( - LIVE_HUB, LIVE_RG, edge_device_ids[0] - ), - checks=[self.exists("sas")], - ) - - self.cmd( - "iot hub generate-sas-token -n {} -g {} -d {} --du {}".format( - LIVE_HUB, LIVE_RG, edge_device_ids[0], "1000" - ), - checks=[self.exists("sas")], - ) - - # None SAS device auth - self.cmd( - "iot hub generate-sas-token -n {} -g {} -d {}".format( - LIVE_HUB, LIVE_RG, device_ids[1] - ), - expect_failure=True, - ) - - self.cmd( - 'iot hub generate-sas-token -n {} -g {} -d {} --kt "secondary"'.format( - LIVE_HUB, LIVE_RG, edge_device_ids[1] - ), - checks=[self.exists("sas")], - ) - - # With connection string - self.cmd( - "iot hub generate-sas-token -d {} --login {}".format( - edge_device_ids[0], self.connection_string - ), - checks=[self.exists("sas")], - ) - - self.cmd( - 'iot hub generate-sas-token -d {} --login {} --kt "secondary"'.format( - edge_device_ids[1], self.connection_string - ), - checks=[self.exists("sas")], - ) - - self.cmd( - 'iot hub generate-sas-token -d {} --login {} --pn "mypolicy"'.format( - edge_device_ids[1], self.connection_string - ), - expect_failure=True, - ) - - -class TestIoTHubDeviceTwins(IoTLiveScenarioTest): - def __init__(self, test_case): - super(TestIoTHubDeviceTwins, self).__init__( - test_case, LIVE_HUB, LIVE_RG - ) - - def test_hub_device_twins(self): - self.kwargs["generic_dict"] = {"key": "value"} - self.kwargs["bad_format"] = "{'key: 'value'}" - self.kwargs["patch_desired"] = {"patchScenario": {"desiredKey": "desiredValue"}} - self.kwargs["patch_tags"] = {"patchScenario": {"tagkey": "tagValue"}} - - device_count = 3 - device_ids = self.generate_device_names(device_count) - - for device in device_ids: - self.cmd( - "iot hub device-identity create -d {} -n {} -g {}".format( - device, LIVE_HUB, LIVE_RG - ), - checks=[self.check("deviceId", device)], - ) - - self.cmd( - "iot hub device-twin show -d {} -n {} -g {}".format( - device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("status", "enabled"), - self.exists("properties.desired"), - self.exists("properties.reported"), - ], - ) - - # With connection string - self.cmd( - "iot hub device-twin show -d {} --login {}".format( - device_ids[0], self.connection_string - ), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("status", "enabled"), - self.exists("properties.desired"), - self.exists("properties.reported"), - ], - ) - - # Patch based twin update of desired props - self.cmd( - "iot hub device-twin update -d {} -n {} -g {} --desired {}".format( - device_ids[2], - LIVE_HUB, - LIVE_RG, - '"{patch_desired}"', - ), - checks=[ - self.check("deviceId", device_ids[2]), - self.check( - "properties.desired.patchScenario", - self.kwargs["patch_desired"]["patchScenario"], - ), - ], - ) - - # Patch based twin update of tags with connection string - self.cmd( - "iot hub device-twin update -d {} --login {} --tags {}".format( - device_ids[2], self.connection_string, '"{patch_tags}"' - ), - checks=[ - self.check("deviceId", device_ids[2]), - self.check( - "tags.patchScenario", self.kwargs["patch_tags"]["patchScenario"] - ), - ], - ) - - # Patch based twin update of desired + tags - self.cmd( - "iot hub device-twin update -d {} -n {} --desired {} --tags {}".format( - device_ids[2], - LIVE_HUB, - '"{patch_desired}"', - '"{patch_tags}"', - ), - checks=[ - self.check("deviceId", device_ids[2]), - self.check( - "properties.desired.patchScenario", - self.kwargs["patch_desired"]["patchScenario"], - ), - self.check( - "tags.patchScenario", - self.kwargs["patch_tags"]["patchScenario"] - ), - ], - ) - - # Deprecated generic update - result = self.cmd( - "iot hub device-twin update -d {} -n {} -g {} --set properties.desired.special={}".format( - device_ids[0], LIVE_HUB, LIVE_RG, '"{generic_dict}"' - ) - ).get_output_in_json() - assert result["deviceId"] == device_ids[0] - assert result["properties"]["desired"]["special"]["key"] == "value" - - # Removal of desired property from twin - result = self.cmd( - 'iot hub device-twin update -d {} -n {} -g {} --set properties.desired.special="null"'.format( - device_ids[0], LIVE_HUB, LIVE_RG - ) - ).get_output_in_json() - assert result["deviceId"] == device_ids[0] - assert result["properties"]["desired"].get("special") is None - - # With connection string - result = self.cmd( - "iot hub device-twin update -d {} --login {} --set properties.desired.special={}".format( - device_ids[0], self.connection_string, '"{generic_dict}"' - ) - ).get_output_in_json() - assert result["deviceId"] == device_ids[0] - assert result["properties"]["desired"]["special"]["key"] == "value" - - # Error case, test type enforcer - self.cmd( - "iot hub device-twin update -d {} -n {} -g {} --set tags={}".format( - device_ids[0], LIVE_HUB, LIVE_RG, '"{bad_format}"' - ), - expect_failure=True, - ) - - content_path = os.path.join(CWD, "test_generic_replace.json") - self.cmd( - "iot hub device-twin replace -d {} -n {} -g {} -j '{}'".format( - device_ids[0], LIVE_HUB, LIVE_RG, content_path - ), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("properties.desired.awesome", 9001), - self.check("properties.desired.temperature.min", 10), - self.check("properties.desired.temperature.max", 100), - self.check("tags.location.region", "US"), - ], - ) - - self.kwargs["twin_payload"] = read_file_content(content_path) - self.cmd( - "iot hub device-twin replace -d {} -n {} -g {} -j '{}'".format( - device_ids[1], LIVE_HUB, LIVE_RG, "{twin_payload}" - ), - checks=[ - self.check("deviceId", device_ids[1]), - self.check("properties.desired.awesome", 9001), - self.check("properties.desired.temperature.min", 10), - self.check("properties.desired.temperature.max", 100), - self.check("tags.location.region", "US"), - ], - ) - - # With connection string - self.cmd( - "iot hub device-twin replace -d {} --login {} -j '{}'".format( - device_ids[1], self.connection_string, "{twin_payload}" - ), - checks=[ - self.check("deviceId", device_ids[1]), - self.check("properties.desired.awesome", 9001), - self.check("properties.desired.temperature.min", 10), - self.check("properties.desired.temperature.max", 100), - self.check("tags.location.region", "US"), - ], - ) - - # Region specific test - if self.region not in ["West US 2", "North Europe", "Southeast Asia"]: - warnings.warn(UserWarning("Skipping distributed-tracing tests. IoT Hub not in supported region!")) - else: - self.cmd( - "iot hub distributed-tracing show -d {} -n {} -g {}".format( - device_ids[2], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - result = self.cmd( - "iot hub distributed-tracing update -d {} -n {} -g {} --sm on --sr 50".format( - device_ids[2], LIVE_HUB, LIVE_RG - ) - ).get_output_in_json() - assert result["deviceId"] == device_ids[2] - assert result["samplingMode"] == "enabled" - assert result["samplingRate"] == "50%" - assert not result["isSynced"] - - -class TestIoTHubModules(IoTLiveScenarioTest): - def __init__(self, test_case): - super(TestIoTHubModules, self).__init__( - test_case, LIVE_HUB, LIVE_RG - ) - - def test_hub_modules(self): - edge_device_count = 2 - device_count = 1 - module_count = 2 - - edge_device_ids = self.generate_device_names(edge_device_count, edge=True) - device_ids = self.generate_device_names(device_count) - module_ids = self.generate_module_names(module_count) - - for edge_device in edge_device_ids: - self.cmd( - "iot hub device-identity create -d {} -n {} -g {} --ee".format( - edge_device, LIVE_HUB, LIVE_RG - ), - checks=[self.check("deviceId", edge_device)], - ) - - self.cmd( - "iot hub device-identity create -d {} -n {} -g {}".format( - device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[self.check("deviceId", device_ids[0])], - ) - - # Symmetric Key - # With connection string - self.cmd( - "iot hub module-identity create --device-id {} --hub-name {} --resource-group {} --module-id {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[1] - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[1]), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - self.cmd( - "iot hub module-identity create -d {} --login {} -m {}".format( - edge_device_ids[0], self.connection_string, module_ids[0] - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - # Error can't get a sas token for module without device - self.cmd( - "az iot hub generate-sas-token -n {} -g {} -m {}".format( - LIVE_HUB, LIVE_RG, module_ids[1] - ), - expect_failure=True, - ) - - # sas token for module - self.cmd( - "iot hub generate-sas-token -n {} -g {} -d {} -m {}".format( - LIVE_HUB, LIVE_RG, edge_device_ids[0], module_ids[1] - ), - checks=[self.exists("sas")], - ) - - # sas token for module with connection string - self.cmd( - "iot hub generate-sas-token -d {} -m {} --login {}".format( - edge_device_ids[0], module_ids[1], self.connection_string - ), - checks=[self.exists("sas")], - ) - - # sas token for module with mixed case connection string - mixed_case_cstring = self.connection_string.replace("HostName", "hostname", 1) - self.cmd( - "iot hub generate-sas-token -d {} -m {} --login {}".format( - edge_device_ids[0], module_ids[1], mixed_case_cstring - ), - checks=[self.exists("sas")], - ) - - # X509 Thumbprint - # With connection string - self.cmd( - """iot hub module-identity create --module-id {} --device-id {} --login {} - --auth-method x509_thumbprint --primary-thumbprint {} --secondary-thumbprint {}""".format( - module_ids[0], - device_ids[0], - self.connection_string, - PRIMARY_THUMBPRINT, - SECONDARY_THUMBPRINT, - ), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("moduleId", module_ids[0]), - self.check("connectionState", "Disconnected"), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.check( - "authentication.x509Thumbprint.primaryThumbprint", - PRIMARY_THUMBPRINT, - ), - self.check( - "authentication.x509Thumbprint.secondaryThumbprint", - SECONDARY_THUMBPRINT, - ), - ], - ) - - self.cmd( - """iot hub module-identity create -m {} -d {} -n {} -g {} --am x509_thumbprint --vd {}""".format( - module_ids[1], device_ids[0], LIVE_HUB, LIVE_RG, 10 - ), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("moduleId", module_ids[1]), - self.check("connectionState", "Disconnected"), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.exists("authentication.x509Thumbprint.primaryThumbprint"), - self.check("authentication.x509Thumbprint.secondaryThumbprint", None), - ], - ) - - # X509 CA - # With connection string - self.cmd( - """iot hub module-identity create --module-id {} --device-id {} --login {} --auth-method x509_ca""".format( - module_ids[0], edge_device_ids[1], self.connection_string - ), - checks=[ - self.check("deviceId", edge_device_ids[1]), - self.check("moduleId", module_ids[0]), - self.check("connectionState", "Disconnected"), - self.check("authentication.symmetricKey.primaryKey", None), - self.check("authentication.symmetricKey.secondaryKey", None), - self.check("authentication.x509Thumbprint.primaryThumbprint", None), - self.check("authentication.x509Thumbprint.secondaryThumbprint", None), - ], - ) - - # Includes $edgeAgent && $edgeHub system modules - result = self.cmd( - 'iot hub query --hub-name {} -g {} -q "{}"'.format( - LIVE_HUB, - LIVE_RG, - "select * from devices.modules where devices.deviceId='{}'".format( - edge_device_ids[0] - ), - ) - ).get_output_in_json() - assert len(result) == 4 - - self.cmd( - '''iot hub module-identity update -d {} -n {} -g {} -m {} - --set authentication.symmetricKey.primaryKey="" authentication.symmetricKey.secondaryKey=""'''.format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0] - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - # With connection string - self.cmd( - '''iot hub module-identity update -d {} --login {} -m {} - --set authentication.symmetricKey.primaryKey="" authentication.symmetricKey.secondaryKey=""'''.format( - edge_device_ids[0], self.connection_string, module_ids[0] - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - self.cmd( - "iot hub module-identity list -d {} -n {} -g {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("length([*])", 4), - self.exists("[?moduleId=='$edgeAgent']"), - self.exists("[?moduleId=='$edgeHub']"), - ], - ) - - self.cmd( - "iot hub module-identity list -d {} -n {} -g {} --top -1".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("length([*])", 3), - self.exists("[?moduleId=='$edgeAgent']"), - self.exists("[?moduleId=='$edgeHub']"), - ], - ) - - # With connection string - self.cmd( - "iot hub module-identity list -d {} --login {}".format( - edge_device_ids[0], self.connection_string - ), - checks=[ - self.check("length([*])", 4), - self.exists("[?moduleId=='$edgeAgent']"), - self.exists("[?moduleId=='$edgeHub']"), - ], - ) - - self.cmd( - "iot hub module-identity show -d {} -n {} -g {} -m {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0] - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - # With connection string - self.cmd( - "iot hub module-identity show -d {} --login {} -m {}".format( - edge_device_ids[0], self.connection_string, module_ids[0] - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - mod_sym_conn_str_pattern = r"^HostName={}\.azure-devices\.net;DeviceId={};ModuleId={};SharedAccessKey=".format( - LIVE_HUB, edge_device_ids[0], module_ids[0] - ) - self.cmd( - "iot hub module-identity show-connection-string -d {} -n {} -g {} -m {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0] - ), - checks=[self.check_pattern("connectionString", mod_sym_conn_str_pattern)], - ) - - # With connection string - self.cmd( - "iot hub module-identity show-connection-string -d {} --login {} -m {}".format( - edge_device_ids[0], self.connection_string, module_ids[0] - ), - checks=[self.check_pattern("connectionString", mod_sym_conn_str_pattern)], - ) - - self.cmd( - "iot hub module-identity show-connection-string -d {} -n {} -g {} -m {} --kt {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0], "secondary" - ), - checks=[self.check_pattern("connectionString", mod_sym_conn_str_pattern)], - ) - - self.cmd( - "iot hub module-identity connection-string show -d {} -n {} -g {} -m {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0] - ), - checks=[self.check_pattern("connectionString", mod_sym_conn_str_pattern)], - ) - - # With connection string - self.cmd( - "iot hub module-identity connection-string show -d {} --login {} -m {}".format( - edge_device_ids[0], self.connection_string, module_ids[0] - ), - checks=[self.check_pattern("connectionString", mod_sym_conn_str_pattern)], - ) - - self.cmd( - "iot hub module-identity connection-string show -d {} -n {} -g {} -m {} --kt {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0], "secondary" - ), - checks=[self.check_pattern("connectionString", mod_sym_conn_str_pattern)], - ) - - for i in module_ids: - if module_ids.index(i) == (module_count - 1): - # With connection string - self.cmd( - "iot hub module-identity delete -d {} --login {} --module-id {}".format( - edge_device_ids[0], self.connection_string, i - ), - checks=self.is_empty(), - ) - else: - self.cmd( - "iot hub module-identity delete -d {} -n {} -g {} --module-id {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, i - ), - checks=self.is_empty(), - ) - - -class TestIoTHubModuleTwins(IoTLiveScenarioTest): - def __init__(self, test_case): - super(TestIoTHubModuleTwins, self).__init__( - test_case, LIVE_HUB, LIVE_RG - ) - - def test_hub_module_twins(self): - self.kwargs["generic_dict"] = {"key": "value"} - self.kwargs["bad_format"] = "{'key: 'value'}" - self.kwargs["patch_desired"] = {"patchScenario": {"desiredKey": "desiredValue"}} - self.kwargs["patch_tags"] = {"patchScenario": {"tagkey": "tagValue"}} - - edge_device_count = 1 - device_count = 1 - module_count = 1 - - edge_device_ids = self.generate_device_names(edge_device_count, True) - device_ids = self.generate_device_names(device_count) - module_ids = self.generate_module_names(module_count) - - self.cmd( - "iot hub device-identity create -d {} -n {} -g {} --ee".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[self.check("deviceId", edge_device_ids[0])], - ) - - self.cmd( - "iot hub device-identity create -d {} -n {} -g {}".format( - device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[self.check("deviceId", device_ids[0])], - ) - - self.cmd( - "iot hub module-identity create -d {} -n {} -g {} -m {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0] - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) - - self.cmd( - "iot hub module-identity create -d {} -n {} -g {} -m {}".format( - device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0] - ), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("moduleId", module_ids[0]), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - ], - ) +from azext_iot.tests.generators import generate_generic_id +# TODO: assert DEVICE_DEVICESCOPE_PREFIX format in parent device twin. +from azext_iot.constants import IOTHUB_TRACK_2_SDK_MIN_VERSION - self.cmd( - "iot hub module-twin show -d {} -n {} -g {} -m {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0] - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.exists("properties.desired"), - self.exists("properties.reported"), - ], - ) - - # With connection string - self.cmd( - "iot hub module-twin show -d {} --login {} -m {}".format( - edge_device_ids[0], self.connection_string, module_ids[0] - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.exists("properties.desired"), - self.exists("properties.reported"), - ], - ) +opt_env_set = ["azext_iot_teststorageuri", "azext_iot_identity_teststorageid"] - # Patch based twin update of desired props - self.cmd( - "iot hub module-twin update -d {} -n {} -g {} -m {} --desired {}".format( - edge_device_ids[0], - LIVE_HUB, - LIVE_RG, - module_ids[0], - '"{patch_desired}"', - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.check( - "properties.desired.patchScenario", - self.kwargs["patch_desired"]["patchScenario"], - ), - ], - ) +settings = DynamoSettings( + req_env_set=ENV_SET_TEST_IOTHUB_BASIC, opt_env_set=opt_env_set +) - # Patch based twin update of tags with connection string - self.cmd( - "iot hub module-twin update -d {} --login {} -m {} --tags {}".format( - edge_device_ids[0], self.connection_string, module_ids[0], '"{patch_tags}"' - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.check( - "tags.patchScenario", self.kwargs["patch_tags"]["patchScenario"] - ), - ], - ) +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg - # Patch based twin update of desired + tags - self.cmd( - "iot hub module-twin update -d {} -n {} -m {} --desired {} --tags {}".format( - device_ids[0], - LIVE_HUB, - module_ids[0], - '"{patch_desired}"', - '"{patch_tags}"', - ), - checks=[ - self.check("deviceId", device_ids[0]), - self.check("moduleId", module_ids[0]), - self.check( - "properties.desired.patchScenario", - self.kwargs["patch_desired"]["patchScenario"], - ), - self.check( - "tags.patchScenario", - self.kwargs["patch_tags"]["patchScenario"] - ), - ], - ) +# Set this environment variable to your empty blob container sas uri to test device export and enable file upload test. +# For file upload, you will need to have configured your IoT Hub before running. +LIVE_STORAGE_URI = settings.env.azext_iot_teststorageuri - # Deprecated twin update style - self.cmd( - "iot hub module-twin update -d {} -n {} -g {} -m {} --set properties.desired.special={}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0], '"{generic_dict}"' - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.check("properties.desired.special.key", "value"), - ], - ) +# Set this environment variable to enable identity-based integration tests +# You will need permissions to add and remove role assignments for this storage account +LIVE_STORAGE_RESOURCE_ID = settings.env.azext_iot_identity_teststorageid - # With connection string - self.cmd( - "iot hub module-twin update -d {} --login {} -m {} --set properties.desired.special={}".format( - edge_device_ids[0], self.connection_string, module_ids[0], '"{generic_dict}"' - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.check("properties.desired.special.key", "value"), - ], - ) +CWD = os.path.dirname(os.path.abspath(__file__)) - # Error case test type enforcer - self.cmd( - "iot hub module-twin update -d {} --login {} -m {} --set properties.desired={}".format( - edge_device_ids[0], self.connection_string, module_ids[0], '"{bad_format}"' - ), - expect_failure=True, - ) +user_managed_identity_name = generate_generic_id() - self.cmd( - "iot hub module-twin update -d {} --login {} -m {} --set tags={}".format( - edge_device_ids[0], self.connection_string, module_ids[0], '"{bad_format}"' - ), - expect_failure=True, - ) - content_path = os.path.join(CWD, "test_generic_replace.json") - self.cmd( - "iot hub module-twin replace -d {} -n {} -g {} -m {} -j '{}'".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0], content_path - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.check("properties.desired.awesome", 9001), - self.check("properties.desired.temperature.min", 10), - self.check("properties.desired.temperature.max", 100), - self.check("tags.location.region", "US"), - ], - ) +class TestIoTStorage(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTStorage, self).__init__(test_case, LIVE_HUB, LIVE_RG) + self.managed_identity = None - # With connection string - self.cmd( - "iot hub module-twin replace -d {} --login {} -m {} -j '{}'".format( - edge_device_ids[0], self.connection_string, module_ids[0], content_path - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.check("properties.desired.awesome", 9001), - self.check("properties.desired.temperature.min", 10), - self.check("properties.desired.temperature.max", 100), - self.check("tags.location.region", "US"), - ], - ) + def get_managed_identity(self): + # Check if there is a managed identity already + if self.managed_identity: + return self.managed_identity - self.kwargs["twin_payload"] = read_file_content(content_path) - self.cmd( - "iot hub module-twin replace -d {} -n {} -g {} -m {} -j '{}'".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, module_ids[0], "{twin_payload}" - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.check("moduleId", module_ids[0]), - self.check("properties.desired.awesome", 9001), - self.check("properties.desired.temperature.min", 10), - self.check("properties.desired.temperature.max", 100), - self.check("tags.location.region", "US"), - ], - ) + # Create managed identity + result = self.cmd( + "identity create -n {} -g {}".format( + user_managed_identity_name, LIVE_RG + )).get_output_in_json() - for i in module_ids: - self.cmd( - "iot hub module-identity delete -d {} -n {} -g {} --module-id {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG, i - ), - checks=self.is_empty(), - ) + # ensure resource is created before hub immediately tries to assign it + sleep(10) + self.managed_identity = result + return self.managed_identity -class TestIoTStorage(IoTLiveScenarioTest): - def __init__(self, test_case): - super(TestIoTStorage, self).__init__(test_case, LIVE_HUB, LIVE_RG) + def tearDown(self): + if self.managed_identity: + self.cmd('identity delete -n {} -g {}'.format( + user_managed_identity_name, LIVE_RG + )) + return super().tearDown() @pytest.mark.skipif( - not LIVE_STORAGE, reason="empty azext_iot_teststorageuri env var" + not LIVE_STORAGE_URI, reason="empty azext_iot_teststorageuri env var" ) def test_storage(self): device_count = 1 @@ -1443,315 +100,245 @@ def test_storage(self): self.cmd( 'iot hub device-identity export -n {} --bcu "{}"'.format( - LIVE_HUB, LIVE_STORAGE + LIVE_HUB, LIVE_STORAGE_URI + ), + checks=[ + self.check("outputBlobContainerUri", LIVE_STORAGE_URI), + self.check("failureReason", None), + self.check("type", "export"), + self.check("excludeKeysInExport", True), + self.exists("jobId"), + ], + ) + + # give time to finish job + sleep(30) + + self.cmd( + 'iot hub device-identity export -n {} --bcu "{}" --auth-type {} --ik true'.format( + LIVE_HUB, LIVE_STORAGE_URI, "key" ), checks=[ - self.check("outputBlobContainerUri", LIVE_STORAGE), + self.check("outputBlobContainerUri", LIVE_STORAGE_URI), self.check("failureReason", None), self.check("type", "export"), + self.check("excludeKeysInExport", False), + self.exists("jobId"), + ], + ) + + # give time to finish job + sleep(30) + + self.cmd( + 'iot hub device-identity import -n {} --ibcu "{}" --obcu "{}" --auth-type {}'.format( + LIVE_HUB, LIVE_STORAGE_URI, LIVE_STORAGE_URI, "key" + ), + checks=[ + self.check("outputBlobContainerUri", LIVE_STORAGE_URI), + self.check("inputBlobContainerUri", LIVE_STORAGE_URI), + self.check("failureReason", None), + self.check("type", "import"), + self.check("storageAuthenticationType", "keyBased"), self.exists("jobId"), ], ) @pytest.mark.skipif( - not all([LIVE_STORAGE_ID, LIVE_STORAGE]), + not all([LIVE_STORAGE_RESOURCE_ID, LIVE_STORAGE_URI]), reason="azext_iot_identity_teststorageid and azext_iot_teststorageuri env vars not set", ) - def test_identity_storage(self): + @pytest.mark.skipif( + not ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION), + reason="Skipping track 2 tests because SDK is track 1") + def test_system_identity_storage(self): identity_type_enable = "SystemAssigned" - identity_type_disable = "None" storage_role = "Storage Blob Data Contributor" # check hub identity identity_enabled = False hub_identity = self.cmd( - "iot hub show -n {}".format(LIVE_HUB) - ).get_output_in_json()["identity"] + "iot hub identity show -n {}".format(LIVE_HUB) + ).get_output_in_json() - if hub_identity.get("type", None) != identity_type_enable: + if identity_type_enable not in hub_identity.get("type", None): # enable hub identity and get ID hub_identity = self.cmd( - 'iot hub update -n {} --set identity.type="{}"'.format( - LIVE_HUB, identity_type_enable + "iot hub identity assign -n {} --system".format( + LIVE_HUB, ) - ).get_output_in_json()["identity"] + ).get_output_in_json() identity_enabled = True + # principal id for system assigned user identity hub_id = hub_identity.get("principalId", None) assert hub_id # setup RBAC for storage account storage_account_roles = self.cmd( 'role assignment list --scope "{}" --role "{}" --query "[].principalId"'.format( - LIVE_STORAGE_ID, storage_role + LIVE_STORAGE_RESOURCE_ID, storage_role ) ).get_output_in_json() if hub_id not in storage_account_roles: self.cmd( 'role assignment create --assignee "{}" --role "{}" --scope "{}"'.format( - hub_id, storage_role, LIVE_STORAGE_ID + hub_id, storage_role, LIVE_STORAGE_RESOURCE_ID ) ) - # give RBAC time to catch up - from time import sleep - sleep(30) + # give time to finish job + sleep(60) - # identity-based device-identity export self.cmd( - 'iot hub device-identity export -n {} --bcu "{}" --auth-type {}'.format( - LIVE_HUB, LIVE_STORAGE, "identity" + 'iot hub device-identity export -n {} --bcu "{}" --auth-type {} --identity {} --ik true'.format( + LIVE_HUB, LIVE_STORAGE_URI, "identity", "[system]" ), checks=[ - self.check("outputBlobContainerUri", LIVE_STORAGE), + self.check("outputBlobContainerUri", LIVE_STORAGE_URI), self.check("failureReason", None), self.check("type", "export"), + self.check("excludeKeysInExport", False), + self.check("storageAuthenticationType", "identityBased"), + self.exists("jobId"), + ], + ) + + self.cmd( + 'iot hub device-identity import -n {} --ibcu "{}" --obcu "{}" --auth-type {} --identity {}'.format( + LIVE_HUB, LIVE_STORAGE_URI, LIVE_STORAGE_URI, "identity", "[system]" + ), + checks=[ + self.check("outputBlobContainerUri", LIVE_STORAGE_URI), + self.check("inputBlobContainerUri", LIVE_STORAGE_URI), + self.check("failureReason", None), + self.check("type", "import"), + self.check("storageAuthenticationType", "identityBased"), self.exists("jobId"), ], ) + self.cmd( + 'iot hub device-identity export -n {} --bcu "{}" --auth-type {} --identity {}'.format( + LIVE_HUB, LIVE_STORAGE_URI, "identity", "fake_managed_identity" + ), + expect_failure=True + ) + # if we enabled identity for this hub, undo identity and RBAC if identity_enabled: # delete role assignment first, disabling identity removes the assignee ID from AAD self.cmd( 'role assignment delete --assignee "{}" --role "{}" --scope "{}"'.format( - hub_id, storage_role, LIVE_STORAGE_ID + hub_id, storage_role, LIVE_STORAGE_RESOURCE_ID ) ) self.cmd( - "iot hub update -n {} --set 'identity.type=\"{}\"'".format( - LIVE_HUB, identity_type_disable + "iot hub identity remove -n {} --system".format( + LIVE_HUB ) ) + @pytest.mark.skipif( + not all([LIVE_STORAGE_RESOURCE_ID, LIVE_STORAGE_URI]), + reason="azext_iot_identity_teststorageid and azext_iot_teststorageuri env vars not set", + ) + @pytest.mark.skipif( + not ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION), + reason="Skipping track 2 tests because SDK is track 1") + def test_user_identity_storage(self): + # User Assigned Managed Identity + storage_role = "Storage Blob Data Contributor" + user_identity = self.get_managed_identity() + identity_id = user_identity["id"] + # check hub identity + identity_enabled = False + hub_identity = self.cmd( + "iot hub identity show -n {}".format(LIVE_HUB) + ).get_output_in_json() -class TestIoTEdgeOffline(IoTLiveScenarioTest): - def __init__(self, test_case): - super(TestIoTEdgeOffline, self).__init__( - test_case, LIVE_HUB, LIVE_RG - ) + if hub_identity.get("userAssignedIdentities", None) != user_identity["principalId"]: + # enable hub identity and get ID + hub_identity = self.cmd( + "iot hub identity assign -n {} --user {}".format( + LIVE_HUB, identity_id + ) + ).get_output_in_json() - def test_edge_offline(self): - device_count = 3 - edge_device_count = 2 + identity_enabled = True - device_ids = self.generate_device_names(device_count) - edge_device_ids = self.generate_device_names(edge_device_count, True) + identity_principal = hub_identity["userAssignedIdentities"][identity_id]["principalId"] + assert identity_principal == user_identity["principalId"] - for edge_device in edge_device_ids: - self.cmd( - "iot hub device-identity create -d {} -n {} -g {} --ee".format( - edge_device, LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", edge_device), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("connectionState", "Disconnected"), - self.check("capabilities.iotEdge", True), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - self.exists("deviceScope"), - ], + # setup RBAC for storage account + storage_account_roles = self.cmd( + 'role assignment list --scope "{}" --role "{}" --query "[].principalId"'.format( + LIVE_STORAGE_RESOURCE_ID, storage_role ) + ).get_output_in_json() - for device in device_ids: + if identity_principal not in storage_account_roles: self.cmd( - "iot hub device-identity create -d {} -n {} -g {}".format( - device, LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", device), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("connectionState", "Disconnected"), - self.check("capabilities.iotEdge", False), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - self.check("deviceScope", None), - ], + 'role assignment create --assignee "{}" --role "{}" --scope "{}"'.format( + identity_principal, storage_role, LIVE_STORAGE_RESOURCE_ID + ) ) + # give time to finish job + sleep(60) - # get-parent of edge device - self.cmd( - "iot hub device-identity get-parent -d {} -n {} -g {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # get-parent of device which doesn't have any parent set - self.cmd( - "iot hub device-identity get-parent -d {} -n {} -g {}".format( - device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # setting non-edge device as a parent of non-edge device - self.cmd( - "iot hub device-identity set-parent -d {} --pd {} -n {} -g {}".format( - device_ids[0], device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # setting edge device as a parent of edge device - self.cmd( - "iot hub device-identity set-parent -d {} --pd {} -n {} -g {}".format( - edge_device_ids[0], edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # add device as a child of non-edge device - self.cmd( - "iot hub device-identity add-children -d {} --child-list {} -n {} -g {}".format( - device_ids[0], device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # add device list as children of edge device - self.cmd( - "iot hub device-identity add-children -d {} --child-list '{}' -n {} -g {}".format( - edge_device_ids[0], ", ".join(device_ids), LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # setting edge device as a parent of non-edge device which already having different parent device - self.cmd( - "iot hub device-identity set-parent -d {} --pd {} -n {} -g {}".format( - device_ids[2], edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # setting edge device as a parent of non-edge device which already having different parent device by force - self.cmd( - "iot hub device-identity set-parent -d {} --pd {} -n {} -g {} --force".format( - device_ids[2], edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # get-parent of device + # identity-based device-identity export self.cmd( - "iot hub device-identity get-parent -d {} -n {} -g {}".format( - device_ids[0], LIVE_HUB, LIVE_RG + 'iot hub device-identity export -n {} --bcu "{}" --auth-type {} --identity {} --ik true'.format( + LIVE_HUB, LIVE_STORAGE_URI, "identity", identity_id ), checks=[ - self.check("deviceId", edge_device_ids[0]), - self.exists("deviceScope"), + self.check("outputBlobContainerUri", LIVE_STORAGE_URI), + self.check("failureReason", None), + self.check("type", "export"), + self.check("excludeKeysInExport", False), + self.check("storageAuthenticationType", "identityBased"), + self.exists("jobId"), ], ) - # add same device as a child of same parent device - self.cmd( - "iot hub device-identity add-children -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[0], device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # add same device as a child of another edge device - self.cmd( - "iot hub device-identity add-children -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[1], device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # add same device as a child of another edge device by force - self.cmd( - "iot hub device-identity add-children -d {} --child-list {} -n {} -g {} --force".format( - edge_device_ids[1], device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # list child devices of edge device - output = self.cmd( - "iot hub device-identity list-children -d {} -n {} -g {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=False, - ) - - expected_output = "{}".format(device_ids[1]) - assert output.get_output_in_json() == expected_output - - # removing all child devices of non-edge device - self.cmd( - "iot hub device-identity remove-children -d {} -n {} -g {} --remove-all".format( - device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # remove all child devices from edge device - self.cmd( - "iot hub device-identity remove-children -d {} -n {} -g {} --remove-all".format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # removing all child devices of edge device which doesn't have any child devices - self.cmd( - "iot hub device-identity remove-children -d {} -n {} -g {} --remove-all".format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # removing child devices of edge device neither passing child devices list nor remove-all parameter - self.cmd( - "iot hub device-identity remove-children -d {} -n {} -g {}".format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # remove edge device from edge device - self.cmd( - "iot hub device-identity remove-children -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[1], edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) + # give time to finish job + sleep(30) - # remove device from edge device but device is a child of another edge device self.cmd( - "iot hub device-identity remove-children -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[1], device_ids[1], LIVE_HUB, LIVE_RG + 'iot hub device-identity import -n {} --ibcu "{}" --obcu "{}" --auth-type {} --identity {}'.format( + LIVE_HUB, LIVE_STORAGE_URI, LIVE_STORAGE_URI, "identity", identity_id ), - expect_failure=True, + checks=[ + self.check("outputBlobContainerUri", LIVE_STORAGE_URI), + self.check("inputBlobContainerUri", LIVE_STORAGE_URI), + self.check("failureReason", None), + self.check("type", "import"), + self.check("storageAuthenticationType", "identityBased"), + self.exists("jobId"), + ], ) - # remove device self.cmd( - "iot hub device-identity remove-children -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[0], device_ids[1], LIVE_HUB, LIVE_RG + 'iot hub device-identity export -n {} --bcu "{}" --auth-type {} --identity {}'.format( + LIVE_HUB, LIVE_STORAGE_URI, "identity", "fake_managed_identity" ), - checks=self.is_empty(), + expect_failure=True ) - # remove device which doesn't have any parent set - self.cmd( - "iot hub device-identity remove-children -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[0], device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) + # if we enabled identity for this hub, undo identity and RBAC + if identity_enabled: + # delete role assignment first, disabling identity removes the assignee ID from AAD + self.cmd( + 'role assignment delete --assignee "{}" --role "{}" --scope "{}"'.format( + identity_principal, storage_role, LIVE_STORAGE_RESOURCE_ID + ) + ) + self.cmd( + "iot hub identity remove -n {} --user".format( + LIVE_HUB + ) + ) - # list child devices of edge device which doesn't have any children - self.cmd( - "iot hub device-identity list-children -d {} -n {} -g {}".format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) + self.tearDown() diff --git a/azext_iot/tests/iothub/test_iot_ext_unit.py b/azext_iot/tests/iothub/test_iot_ext_unit.py index 73ea3f944..a668dce82 100644 --- a/azext_iot/tests/iothub/test_iot_ext_unit.py +++ b/azext_iot/tests/iothub/test_iot_ext_unit.py @@ -32,7 +32,7 @@ mock_target, generate_cs, ) - +from azext_iot.common.shared import DeviceAuthApiType device_id = "mydevice" child_device_id = "child_device1" @@ -58,9 +58,6 @@ def generate_device_create_req( status_reason=None, valid_days=None, output_dir=None, - set_parent_id=None, - add_children=None, - force=False, ): return { "client": None, @@ -73,10 +70,7 @@ def generate_device_create_req( "status": status, "status_reason": status_reason, "valid_days": valid_days, - "output_dir": output_dir, - "set_parent_id": set_parent_id, - "add_children": add_children, - "force": force, + "output_dir": output_dir } @@ -134,13 +128,13 @@ def test_device_create(self, serviceclient, req): assert body["capabilities"]["iotEdge"] == req["ee"] if req["auth"] == "shared_private_key": - assert body["authentication"]["type"] == "sas" + assert body["authentication"]["type"] == DeviceAuthApiType.sas.value elif req["auth"] == "x509_ca": - assert body["authentication"]["type"] == "certificateAuthority" + assert body["authentication"]["type"] == DeviceAuthApiType.certificateAuthority.value assert not body["authentication"].get("x509Thumbprint") assert not body["authentication"].get("symmetricKey") elif req["auth"] == "x509_thumbprint": - assert body["authentication"]["type"] == "selfSigned" + assert body["authentication"]["type"] == DeviceAuthApiType.selfSigned.value x509tp = body["authentication"]["x509Thumbprint"] assert x509tp["primaryThumbprint"] if req["stp"] is None: @@ -148,168 +142,6 @@ def test_device_create(self, serviceclient, req): else: assert x509tp["secondaryThumbprint"] == req["stp"] - @pytest.fixture - def sc_device_create_setparent(self, mocker, fixture_ghcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - test_side_effect = [ - build_mock_response(mocker, 200, generate_parent_device()), - build_mock_response(mocker, 200, {}), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize( - "req", [(generate_device_create_req(set_parent_id=device_id))] - ) - def test_device_create_setparent(self, sc_device_create_setparent, req): - subject.iot_device_create( - fixture_cmd, - child_device_id, - req["hub_name"], - req["ee"], - req["auth"], - req["ptp"], - req["stp"], - req["status"], - req["status_reason"], - req["valid_days"], - req["output_dir"], - req["set_parent_id"], - ) - - args = sc_device_create_setparent.call_args - url = args[0][0].url - body = json.loads(args[0][0].body) - - assert "{}/devices/{}?".format(mock_target["entity"], child_device_id) in url - assert args[0][0].method == "PUT" - - assert body["deviceId"] == child_device_id - assert body["deviceScope"] == generate_parent_device().get("deviceScope") - - @pytest.fixture(params=[(200, 0)]) - def sc_invalid_args_device_create_setparent( - self, mocker, fixture_ghcs, fixture_sas, request - ): - service_client = mocker.patch(path_service_client) - parent_kvp = {} - if request.param[1] == 0: - parent_kvp.setdefault("capabilities", {"iotEdge": False}) - test_side_effect = [ - build_mock_response( - mocker, request.param[0], generate_parent_device(**parent_kvp) - ) - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize("req, exp", [(generate_device_create_req(), CLIError)]) - def test_device_create_setparent_invalid_args( - self, sc_invalid_args_device_create_setparent, req, exp - ): - with pytest.raises(exp): - subject.iot_device_create( - fixture_cmd, - child_device_id, - req["hub_name"], - req["ee"], - req["auth"], - req["ptp"], - req["stp"], - req["status"], - req["status_reason"], - req["valid_days"], - req["output_dir"], - device_id, - ) - - @pytest.fixture(params=[(200, 0), (200, 1), (200, 1)]) - def sc_device_create_addchildren(self, mocker, fixture_ghcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - child_kvp = {} - if request.param[1] == 1: - child_kvp.setdefault("parentScopes", ["abcd"]) - if request.param[1] == 1: - child_kvp.setdefault("capabilities", {"iotEdge": True}) - test_side_effect = [ - build_mock_response( - mocker, request.param[0], generate_child_device(**child_kvp) - ), - build_mock_response(mocker, request.param[0], generate_parent_device()), - build_mock_response( - mocker, request.param[0], generate_child_device(**child_kvp) - ), - build_mock_response(mocker, request.param[0], {}), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize("req", [(generate_device_create_req())]) - def test_device_create_addchildren(self, sc_device_create_addchildren, req): - subject.iot_device_create( - fixture_cmd, - req["device_id"], - req["hub_name"], - True, - req["auth"], - req["ptp"], - req["stp"], - req["status"], - req["status_reason"], - req["valid_days"], - req["output_dir"], - None, - child_device_id, - True, - ) - - args = sc_device_create_addchildren.call_args - url = args[0][0].url - body = json.loads(args[0][0].body) - assert "{}/devices/{}?".format(mock_target["entity"], child_device_id) in url - assert args[0][0].method == "PUT" - assert body["deviceId"] == child_device_id - assert body["deviceScope"] == generate_parent_device().get( - "deviceScope" - ) or body["parentScopes"] == [generate_parent_device().get("deviceScope")] - - @pytest.fixture(params=[(200, 0)]) - def sc_invalid_args_device_create_addchildren( - self, mocker, fixture_ghcs, fixture_sas, request - ): - service_client = mocker.patch(path_service_client) - child_kvp = {} - child_kvp.setdefault("parentScopes", ["abcd"]) - test_side_effect = [ - build_mock_response( - mocker, request.param[0], generate_child_device(**child_kvp) - ) - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize("req, exp", [(generate_device_create_req(), CLIError)]) - def test_device_create_addchildren_invalid_args( - self, sc_invalid_args_device_create_addchildren, req, exp - ): - with pytest.raises(exp): - subject.iot_device_create( - fixture_cmd, - req["device_id"], - req["hub_name"], - True, - req["auth"], - req["ptp"], - req["stp"], - req["status"], - req["status_reason"], - req["valid_days"], - req["output_dir"], - None, - child_device_id, - False, - ) - @pytest.mark.parametrize( "req, exp", [ @@ -357,7 +189,7 @@ def generate_device_show(**kvp): "authentication": { "symmetricKey": {"primaryKey": None, "secondaryKey": None}, "x509Thumbprint": {"primaryThumbprint": None, "secondaryThumbprint": None}, - "type": "sas", + "type": DeviceAuthApiType.sas.value, }, "capabilities": {"iotEdge": True}, "deviceId": device_id, @@ -409,7 +241,7 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): generate_device_show( authentication={ "symmetricKey": {"primaryKey": "", "secondaryKey": ""}, - "type": "sas", + "type": DeviceAuthApiType.sas.value, } ) ), @@ -420,13 +252,13 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): "primaryThumbprint": "123", "secondaryThumbprint": "321", }, - "type": "selfSigned", + "type": DeviceAuthApiType.selfSigned.value, } ) ), ( generate_device_show( - authentication={"type": "certificateAuthority"}, + authentication={"type": DeviceAuthApiType.certificateAuthority.value}, etag=generate_generic_id(), ) ), @@ -451,10 +283,10 @@ def test_device_update(self, fixture_cmd, serviceclient, req): assert body["status"] == req["status"] assert body["capabilities"]["iotEdge"] == req["capabilities"]["iotEdge"] assert req["authentication"]["type"] == body["authentication"]["type"] - if req["authentication"]["type"] == "certificateAuthority": + if req["authentication"]["type"] == DeviceAuthApiType.certificateAuthority.value: assert not body["authentication"].get("x509Thumbprint") assert not body["authentication"].get("symmetricKey") - elif req["authentication"]["type"] == "selfSigned": + elif req["authentication"]["type"] == DeviceAuthApiType.selfSigned.value: assert body["authentication"]["x509Thumbprint"]["primaryThumbprint"] assert body["authentication"]["x509Thumbprint"]["secondaryThumbprint"] @@ -485,7 +317,7 @@ def test_device_update(self, fixture_cmd, serviceclient, req): ( generate_device_show( authentication={ - "type": "selfSigned", + "type": DeviceAuthApiType.selfSigned.value, "symmetricKey": {"primaryKey": None, "secondaryKey": None}, "x509Thumbprint": { "primaryThumbprint": "123", @@ -502,7 +334,7 @@ def test_device_update(self, fixture_cmd, serviceclient, req): ( generate_device_show( authentication={ - "type": "certificateAuthority", + "type": DeviceAuthApiType.certificateAuthority.value, "symmetricKey": {"primaryKey": None, "secondaryKey": None}, "x509Thumbprint": { "primaryThumbprint": None, @@ -521,7 +353,7 @@ def test_device_update(self, fixture_cmd, serviceclient, req): ( generate_device_show( authentication={ - "type": "selfSigned", + "type": DeviceAuthApiType.selfSigned.value, "symmetricKey": {"primaryKey": None, "secondaryKey": None}, "x509Thumbprint": { "primaryThumbprint": "123", @@ -554,7 +386,7 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): assert instance["statusReason"] == arg["status_reason"] if arg["auth_method"]: if arg["auth_method"] == "shared_private_key": - assert instance["authentication"]["type"] == "sas" + assert instance["authentication"]["type"] == DeviceAuthApiType.sas.value instance["authentication"]["symmetricKey"]["primaryKey"] == arg[ "primary_key" ] @@ -562,7 +394,7 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): "secondary_key" ] if arg["auth_method"] == "x509_thumbprint": - assert instance["authentication"]["type"] == "selfSigned" + assert instance["authentication"]["type"] == DeviceAuthApiType.selfSigned.value if arg["primary_thumbprint"]: instance["authentication"]["x509Thumbprint"][ "primaryThumbprint" @@ -572,7 +404,7 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): "secondaryThumbprint" ] = arg["secondary_thumbprint"] if arg["auth_method"] == "x509_ca": - assert instance["authentication"]["type"] == "certificateAuthority" + assert instance["authentication"]["type"] == DeviceAuthApiType.certificateAuthority.value @pytest.mark.parametrize( "req, arg, exp", @@ -602,7 +434,7 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): ( generate_device_show( authentication={ - "type": "selfSigned", + "type": DeviceAuthApiType.selfSigned.value, "symmetricKey": {"primaryKey": None, "secondaryKey": None}, "x509Thumbprint": { "primaryThumbprint": "123", @@ -639,7 +471,7 @@ def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): "primaryThumbprint": "", "secondaryThumbprint": "", }, - "type": "selfSigned", + "type": DeviceAuthApiType.selfSigned.value, } ), CLIError, @@ -676,7 +508,7 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): "authentication", { "symmetricKey": {"primaryKey": "123", "secondaryKey": "321"}, - "type": "sas", + "type": DeviceAuthApiType.sas.value, }, ) test_side_effect = [ @@ -902,13 +734,13 @@ def test_device_module_create(self, serviceclient, req): assert body["moduleId"] == req["module_id"] if req["auth"] == "shared_private_key": - assert body["authentication"]["type"] == "sas" + assert body["authentication"]["type"] == DeviceAuthApiType.sas.value elif req["auth"] == "x509_ca": - assert body["authentication"]["type"] == "certificateAuthority" + assert body["authentication"]["type"] == DeviceAuthApiType.certificateAuthority.value assert not body["authentication"].get("x509Thumbprint") assert not body["authentication"].get("symmetricKey") elif req["auth"] == "x509_thumbprint": - assert body["authentication"]["type"] == "selfSigned" + assert body["authentication"]["type"] == DeviceAuthApiType.selfSigned.value x509tp = body["authentication"]["x509Thumbprint"] assert x509tp["primaryThumbprint"] if req["stp"] is None: @@ -948,7 +780,7 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): generate_device_module_show( authentication={ "symmetricKey": {"primaryKey": "", "secondaryKey": ""}, - "type": "sas", + "type": DeviceAuthApiType.sas.value, }, etag=generate_generic_id(), ) @@ -960,13 +792,13 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): "primaryThumbprint": "123", "secondaryThumbprint": "321", }, - "type": "selfSigned", + "type": DeviceAuthApiType.selfSigned.value, } ) ), ( generate_device_module_show( - authentication={"type": "certificateAuthority"} + authentication={"type": DeviceAuthApiType.certificateAuthority.value} ) ), ], @@ -995,10 +827,10 @@ def test_device_module_update(self, serviceclient, req): assert body["moduleId"] == req["moduleId"] assert not body.get("capabilities") assert req["authentication"]["type"] == body["authentication"]["type"] - if req["authentication"]["type"] == "certificateAuthority": + if req["authentication"]["type"] == DeviceAuthApiType.certificateAuthority.value: assert not body["authentication"].get("x509Thumbprint") assert not body["authentication"].get("symmetricKey") - elif req["authentication"]["type"] == "selfSigned": + elif req["authentication"]["type"] == DeviceAuthApiType.selfSigned.value: assert body["authentication"]["x509Thumbprint"]["primaryThumbprint"] assert body["authentication"]["x509Thumbprint"]["secondaryThumbprint"] @@ -1016,7 +848,7 @@ def test_device_module_update(self, serviceclient, req): "primaryThumbprint": "", "secondaryThumbprint": "", }, - "type": "selfSigned", + "type": DeviceAuthApiType.selfSigned.value, } ), CLIError, @@ -2166,7 +1998,7 @@ def test_generate_sas_token(self): class TestDeviceSimulate: @pytest.fixture(params=[204]) - def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request, fixture_device): + def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request, fixture_device, fixture_iot_device_show_sas): service_client = mocker.patch(path_service_client) service_client.return_value = build_mock_response(mocker, request.param, {}) return service_client @@ -2285,6 +2117,12 @@ def test_device_simulate_mqtt_error(self, mqttclient_generic_error): fixture_cmd, device_id, hub_name=mock_target["entity"] ) + def test_device_simulate_mqtt_non_sas_device_error(self, fixture_ghcs, fixture_self_signed_device_show_self_signed): + with pytest.raises(CLIError): + subject.iot_simulate_device( + fixture_cmd, device_id, hub_name=mock_target["entity"] + ) + class TestMonitorEvents: @pytest.fixture(params=[200]) @@ -2568,7 +2406,7 @@ def sc_addchildren(self, mocker, fixture_ghcs, fixture_sas, request): def test_device_children_add(self, sc_addchildren): subject.iot_device_children_add( - None, device_id, child_device_id, True, mock_target["entity"] + None, device_id, [child_device_id], True, mock_target["entity"] ) args = sc_addchildren.call_args url = args[0][0].url @@ -2604,7 +2442,7 @@ def sc_invalid_args_addchildren(self, mocker, fixture_ghcs, fixture_sas, request def test_device_addchildren_invalid_args(self, sc_invalid_args_addchildren, exp): with pytest.raises(exp): subject.iot_device_children_add( - fixture_cmd, device_id, child_device_id, False, mock_target["entity"] + fixture_cmd, device_id, [child_device_id], False, mock_target["entity"] ) @pytest.fixture(params=[(200, 400), (200, 401), (200, 500)]) @@ -2621,7 +2459,7 @@ def sc_addchildren_error(self, mocker, fixture_ghcs, fixture_sas, request): def test_device_addchildren_error(self, sc_addchildren_error): with pytest.raises(CLIError): subject.iot_device_children_add( - fixture_cmd, device_id, child_device_id, True, mock_target["entity"] + fixture_cmd, device_id, [child_device_id], True, mock_target["entity"] ) @pytest.fixture(params=[(200, 200)]) @@ -2642,7 +2480,7 @@ def sc_invalid_etag_addchildren(self, mocker, fixture_ghcs, fixture_sas, request def test_device_addchildren_invalid_etag(self, sc_invalid_etag_setparent, exp): with pytest.raises(exp): subject.iot_device_children_add( - fixture_cmd, device_id, child_device_id, True, mock_target["entity"] + fixture_cmd, device_id, [child_device_id], True, mock_target["entity"] ) # list-children @@ -2715,7 +2553,7 @@ def sc_removechildrenlist(self, mocker, fixture_ghcs, fixture_sas, request): def test_device_children_remove_list(self, sc_removechildrenlist): subject.iot_device_children_remove( - fixture_cmd, device_id, child_device_id, False, mock_target["entity"] + fixture_cmd, device_id, [child_device_id], False, mock_target["entity"] ) args = sc_removechildrenlist.call_args url = args[0][0].url @@ -2754,7 +2592,7 @@ def test_device_removechildrenlist_invalid_args( ): with pytest.raises(exp): subject.iot_device_children_remove( - fixture_cmd, device_id, child_device_id, False, mock_target["entity"] + fixture_cmd, device_id, [child_device_id], False, mock_target["entity"] ) @pytest.fixture(params=[(200, 200)]) @@ -2783,7 +2621,7 @@ def test_device_removechildrenlist_invalid_etag( ): with pytest.raises(exp): subject.iot_device_children_remove( - fixture_cmd, device_id, child_device_id, False, mock_target["entity"] + fixture_cmd, device_id, [child_device_id], False, mock_target["entity"] ) @pytest.fixture(params=[(200, 400), (200, 401), (200, 500)]) diff --git a/azext_iot/tests/iothub/test_iot_hub_unit.py b/azext_iot/tests/iothub/test_iot_hub_unit.py new file mode 100644 index 000000000..fd37e6325 --- /dev/null +++ b/azext_iot/tests/iothub/test_iot_hub_unit.py @@ -0,0 +1,341 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import re +import pytest +import responses +import json +from knack.cli import CLIError +from azext_iot.operations import hub as subject +from azext_iot.tests.generators import generate_generic_id +from azext_iot.common.utility import ensure_iothub_sdk_min_version +from azext_iot.constants import IOTHUB_TRACK_2_SDK_MIN_VERSION + +hub_name = "HUBNAME" +blob_container_uri = "https://example.com" +resource_group_name = "RESOURCEGROUP" +managed_identity = "EXAMPLEMANAGEDIDENTITY" +generic_job_response = {"JobResponse": generate_generic_id()} +qualified_hostname = "{}.subdomain.domain".format(hub_name) + + +@pytest.fixture +def get_mgmt_client(mocker, fixture_cmd): + from azure.mgmt.iothub import IotHubClient + + # discovery call to find iothub + patch_discovery = mocker.patch( + "azext_iot.iothub.providers.discovery.IotHubDiscovery.get_target" + ) + patch_discovery.return_value = { + "resourcegroup": resource_group_name + } + + # raw token for login credentials + patched_get_raw_token = mocker.patch( + "azure.cli.core._profile.Profile.get_raw_token" + ) + patched_get_raw_token.return_value = ( + mocker.MagicMock(name="creds"), + mocker.MagicMock(name="subscription"), + mocker.MagicMock(name="tenant"), + ) + + patched_get_login_credentials = mocker.patch( + "azure.cli.core._profile.Profile.get_login_credentials" + ) + patched_get_login_credentials.return_value = ( + mocker.MagicMock(name="subscription"), + mocker.MagicMock(name="tenant"), + ) + + patch = mocker.patch( + "azext_iot._factory.iot_hub_service_factory" + ) + # pylint: disable=no-value-for-parameter, unexpected-keyword-arg + if ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION): + patch.return_value = IotHubClient( + credential='', + subscription_id="00000000-0000-0000-0000-000000000000", + ).iot_hub_resource + else: + patch.return_value = IotHubClient( + credentials='', + subscription_id="00000000-0000-0000-0000-000000000000", + ).iot_hub_resource + + return patch + + +def generate_device_identity(include_keys=False, auth_type=None, identity=None, rg=None): + return { + "include_keys": include_keys, + "storage_authentication_type": auth_type, + "identity": identity, + "resource_group_name": rg + } + + +def assert_device_identity_result(actual, expected): + # the body from the call will be put into additional_properties + assert actual.job_id is None + assert actual.start_time_utc is None + assert actual.end_time_utc is None + assert actual.type is None + assert actual.status is None + assert actual.failure_reason is None + assert actual.status_message is None + assert actual.parent_job_id is None + assert actual.additional_properties == expected + + +class TestIoTHubDeviceIdentityExport(object): + @pytest.fixture + def service_client(self, mocked_response, get_mgmt_client): + mocked_response.assert_all_requests_are_fired = False + + mocked_response.add( + method=responses.GET, + content_type="application/json", + url=re.compile( + "https://(.*)management.azure.com/subscriptions/(.*)/" + "providers/Microsoft.Devices/IotHubs" + ), + status=200, + match_querystring=False, + body=json.dumps({"hostName": qualified_hostname}), + ) + + mocked_response.add( + method=responses.POST, + url=re.compile( + "https://management.azure.com/subscriptions/(.*)/" + "providers/Microsoft.Devices/IotHubs/{}/exportDevices".format( + hub_name + ) + ), + body=json.dumps(generic_job_response), + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "req", + [ + generate_device_identity(), + generate_device_identity(include_keys=True), + generate_device_identity(auth_type="identity"), + generate_device_identity(auth_type="key"), + generate_device_identity(rg=resource_group_name), + ] + ) + def test_device_identity_export_track1(self, fixture_cmd, service_client, req): + result = subject.iot_device_export( + cmd=fixture_cmd, + hub_name=hub_name, + blob_container_uri=blob_container_uri, + include_keys=req["include_keys"], + storage_authentication_type=req["storage_authentication_type"], + resource_group_name=req["resource_group_name"], + ) + + request = service_client.calls[0].request + request_body = json.loads(request.body) + + assert request_body["exportBlobContainerUri"] == blob_container_uri + assert request_body["excludeKeys"] == (not req["include_keys"]) + if req["storage_authentication_type"]: + assert request_body["authenticationType"] == req["storage_authentication_type"] + "Based" + if req["storage_authentication_type"] == "identityBased" and req["identity"] not in (None, "[system]"): + assert request_body["identity"]["userAssignedIdentity"] == req["identity"] + + assert_device_identity_result(result, generic_job_response) + + @pytest.mark.parametrize( + "req", + [ + generate_device_identity(), + generate_device_identity(include_keys=True), + generate_device_identity(auth_type="identity"), + generate_device_identity(auth_type="key"), + generate_device_identity(rg=resource_group_name), + generate_device_identity(auth_type="identity", identity="[system]"), + generate_device_identity(auth_type="identity", identity="system"), + generate_device_identity(auth_type="identity", identity="managed_identity"), + ] + ) + @pytest.mark.skipif( + not ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION), + reason="Skipping track 2 tests because SDK is track 1") + def test_device_identity_export_track2(self, fixture_cmd, service_client, req): + result = subject.iot_device_export( + cmd=fixture_cmd, + hub_name=hub_name, + blob_container_uri=blob_container_uri, + include_keys=req["include_keys"], + storage_authentication_type=req["storage_authentication_type"], + identity=req["identity"], + resource_group_name=req["resource_group_name"], + ) + + request = service_client.calls[0].request + request_body = json.loads(request.body) + + assert request_body["exportBlobContainerUri"] == blob_container_uri + assert request_body["excludeKeys"] == (not req["include_keys"]) + if req["storage_authentication_type"]: + assert request_body["authenticationType"] == req["storage_authentication_type"] + "Based" + if req["storage_authentication_type"] == "identityBased" and req["identity"] not in (None, "[system]"): + assert request_body["identity"]["userAssignedIdentity"] == req["identity"] + + assert_device_identity_result(result, generic_job_response) + + @pytest.mark.parametrize( + "req", + [ + generate_device_identity(auth_type="key", identity="[system]"), + generate_device_identity(auth_type="key", identity="system"), + ] + ) + @pytest.mark.skipif( + not ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION), + reason="Skipping track 2 tests because SDK is track 1") + def test_device_identity_export_input(self, fixture_cmd, req): + with pytest.raises(CLIError): + subject.iot_device_export( + cmd=fixture_cmd, + hub_name=hub_name, + blob_container_uri=blob_container_uri, + include_keys=req["include_keys"], + storage_authentication_type=req["storage_authentication_type"], + identity=req["identity"], + resource_group_name=req["resource_group_name"], + ) + + +class TestIoTHubDeviceIdentityImport(object): + @pytest.fixture + def service_client(self, mocked_response, get_mgmt_client): + mocked_response.assert_all_requests_are_fired = False + + mocked_response.add( + method=responses.GET, + content_type="application/json", + url=re.compile( + "https://(.*)management.azure.com/subscriptions/(.*)/" + "providers/Microsoft.Devices/IotHubs" + ), + status=200, + match_querystring=False, + body=json.dumps({"hostName": qualified_hostname}), + ) + + mocked_response.add( + method=responses.POST, + content_type="application/json", + url=re.compile( + "https://management.azure.com/subscriptions/(.*)/" + "providers/Microsoft.Devices/IotHubs/{}/importDevices".format( + hub_name + ) + ), + status=200, + match_querystring=False, + body=json.dumps(generic_job_response), + ) + + yield mocked_response + + @pytest.mark.parametrize( + "req", + [ + generate_device_identity(), + generate_device_identity(auth_type="identity"), + generate_device_identity(auth_type="key"), + generate_device_identity(rg=resource_group_name), + ] + ) + def test_device_identity_import_track1(self, fixture_cmd, service_client, req): + result = subject.iot_device_import( + cmd=fixture_cmd, + hub_name=hub_name, + input_blob_container_uri=blob_container_uri, + output_blob_container_uri=blob_container_uri + "2", + storage_authentication_type=req["storage_authentication_type"], + resource_group_name=req["resource_group_name"], + ) + request = service_client.calls[0].request + request_body = json.loads(request.body) + + assert request_body["inputBlobContainerUri"] == blob_container_uri + assert request_body["outputBlobContainerUri"] == blob_container_uri + "2" + if req["storage_authentication_type"]: + assert request_body["authenticationType"] == req["storage_authentication_type"] + "Based" + if req["storage_authentication_type"] == "identityBased" and req["identity"] not in (None, "[system]"): + assert request_body["identity"]["userAssignedIdentity"] == req["identity"] + + assert_device_identity_result(result, generic_job_response) + + @pytest.mark.parametrize( + "req", + [ + generate_device_identity(), + generate_device_identity(auth_type="identity"), + generate_device_identity(auth_type="key"), + generate_device_identity(rg=resource_group_name), + generate_device_identity(auth_type="identity", identity="[system]"), + generate_device_identity(auth_type="identity", identity="managed_identity"), + ] + ) + @pytest.mark.skipif( + not ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION), + reason="Skipping track 2 tests because SDK is track 1") + def test_device_identity_import_track2(self, fixture_cmd, service_client, req): + result = subject.iot_device_import( + cmd=fixture_cmd, + hub_name=hub_name, + input_blob_container_uri=blob_container_uri, + output_blob_container_uri=blob_container_uri + "2", + storage_authentication_type=req["storage_authentication_type"], + identity=req["identity"], + resource_group_name=req["resource_group_name"], + ) + request = service_client.calls[0].request + request_body = json.loads(request.body) + + assert request_body["inputBlobContainerUri"] == blob_container_uri + assert request_body["outputBlobContainerUri"] == blob_container_uri + "2" + if req["storage_authentication_type"]: + assert request_body["authenticationType"] == req["storage_authentication_type"] + "Based" + if req["storage_authentication_type"] == "identityBased" and req["identity"] not in (None, "[system]"): + assert request_body["identity"]["userAssignedIdentity"] == req["identity"] + + assert_device_identity_result(result, generic_job_response) + + @pytest.mark.parametrize( + "req", + [ + generate_device_identity(auth_type="key", identity="[system]"), + generate_device_identity(auth_type="key", identity="managed_identity"), + ] + ) + @pytest.mark.skipif( + not ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION), + reason="Skipping track 2 tests because SDK is track 1") + def test_device_identity_import_input(self, fixture_cmd, req): + with pytest.raises(CLIError): + subject.iot_device_import( + cmd=fixture_cmd, + hub_name=hub_name, + input_blob_container_uri=blob_container_uri, + output_blob_container_uri=blob_container_uri + "2", + storage_authentication_type=req["storage_authentication_type"], + identity=req["identity"], + resource_group_name=req["resource_group_name"], + ) diff --git a/azext_iot/tests/iothub/test_iothub_nested_edge_int.py b/azext_iot/tests/iothub/test_iothub_nested_edge_int.py deleted file mode 100644 index dee8cb20e..000000000 --- a/azext_iot/tests/iothub/test_iothub_nested_edge_int.py +++ /dev/null @@ -1,243 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from azext_iot.tests import IoTLiveScenarioTest -from ..settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC - -settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) -LIVE_HUB = settings.env.azext_iot_testhub -LIVE_RG = settings.env.azext_iot_testrg - - -class TestIoTNestedEdge(IoTLiveScenarioTest): - def __init__(self, test_case): - super(TestIoTNestedEdge, self).__init__( - test_case, LIVE_HUB, LIVE_RG - ) - - def test_nested_edge(self): - device_count = 3 - edge_device_count = 2 - - device_ids = self.generate_device_names(device_count) - edge_device_ids = self.generate_device_names(edge_device_count, True) - - for edge_device in edge_device_ids: - self.cmd( - "iot hub device-identity create -d {} -n {} -g {} --ee".format( - edge_device, LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", edge_device), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("connectionState", "Disconnected"), - self.check("capabilities.iotEdge", True), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - self.exists("deviceScope"), - ], - ) - - for device in device_ids: - self.cmd( - "iot hub device-identity create -d {} -n {} -g {}".format( - device, LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", device), - self.check("status", "enabled"), - self.check("statusReason", None), - self.check("connectionState", "Disconnected"), - self.check("capabilities.iotEdge", False), - self.exists("authentication.symmetricKey.primaryKey"), - self.exists("authentication.symmetricKey.secondaryKey"), - self.check("deviceScope", None), - ], - ) - - # get parent of edge device - self.cmd( - "iot hub device-identity parent show -d {} -n {} -g {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # get parent of device which doesn't have any parent set - self.cmd( - "iot hub device-identity parent show -d {} -n {} -g {}".format( - device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # setting non-edge device as a parent of non-edge device - self.cmd( - "iot hub device-identity parent set -d {} --pd {} -n {} -g {}".format( - device_ids[0], device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # setting edge device as a parent of edge device - self.cmd( - "iot hub device-identity parent set -d {} --pd {} -n {} -g {}".format( - edge_device_ids[0], edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # add device as a child of non-edge device - self.cmd( - "iot hub device-identity children add -d {} --child-list {} -n {} -g {}".format( - device_ids[0], device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # add device list as children of edge device - self.cmd( - "iot hub device-identity children add -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[0], " ".join(device_ids), LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # setting edge device as a parent of non-edge device which already having different parent device - self.cmd( - "iot hub device-identity parent set -d {} --pd {} -n {} -g {}".format( - device_ids[2], edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # setting edge device as a parent of non-edge device which already having different parent device by force - self.cmd( - "iot hub device-identity parent set -d {} --pd {} -n {} -g {} --force".format( - device_ids[2], edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # get parent of device - self.cmd( - "iot hub device-identity parent show -d {} -n {} -g {}".format( - device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=[ - self.check("deviceId", edge_device_ids[0]), - self.exists("deviceScope"), - ], - ) - - # add same device as a child of same parent device - self.cmd( - "iot hub device-identity children add -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[0], device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # add same device as a child of another edge device - self.cmd( - "iot hub device-identity children add -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[1], device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # add same device as a child of another edge device by force - self.cmd( - "iot hub device-identity children add -d {} --child-list {} -n {} -g {} --force".format( - edge_device_ids[1], device_ids[0], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # list child devices of edge device - output = self.cmd( - "iot hub device-identity children list -d {} -n {} -g {}".format( - edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=False, - ) - - assert output.get_output_in_json() == [device_ids[1]] - - # removing all child devices of non-edge device - self.cmd( - "iot hub device-identity children remove -d {} -n {} -g {} --remove-all".format( - device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # remove all child devices from edge device - self.cmd( - "iot hub device-identity children remove -d {} -n {} -g {} --remove-all".format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # removing all child devices of edge device which doesn't have any child devices - self.cmd( - "iot hub device-identity children remove -d {} -n {} -g {} --remove-all".format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # removing child devices of edge device neither passing child devices list nor remove-all parameter - self.cmd( - "iot hub device-identity children remove -d {} -n {} -g {}".format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # remove edge device from edge device - self.cmd( - "iot hub device-identity children remove -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[1], edge_device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # remove device from edge device but device is a child of another edge device - self.cmd( - "iot hub device-identity children remove -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[1], device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # remove device - self.cmd( - "iot hub device-identity children remove -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[0], device_ids[1], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - # remove device which doesn't have any parent set - self.cmd( - "iot hub device-identity children remove -d {} --child-list {} -n {} -g {}".format( - edge_device_ids[0], device_ids[0], LIVE_HUB, LIVE_RG - ), - expect_failure=True, - ) - - # list child devices of edge device which doesn't have any children - output = self.cmd( - "iot hub device-identity children list -d {} -n {} -g {}".format( - edge_device_ids[1], LIVE_HUB, LIVE_RG - ), - expect_failure=False, - ) - - assert output.get_output_in_json() == [] diff --git a/azext_iot/tests/iothub/test_iothub_utilities_int.py b/azext_iot/tests/iothub/test_iothub_utilities_int.py new file mode 100644 index 000000000..992bd0c60 --- /dev/null +++ b/azext_iot/tests/iothub/test_iothub_utilities_int.py @@ -0,0 +1,153 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.tests.iothub import DATAPLANE_AUTH_TYPES + +settings = DynamoSettings(req_env_set=ENV_SET_TEST_IOTHUB_BASIC) + +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg + + +class TestIoTHubUtilities(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTHubUtilities, self).__init__(test_case, LIVE_HUB, LIVE_RG) + + def test_iothub_generate_sas_token(self): + for auth_phase in DATAPLANE_AUTH_TYPES: + self.cmd( + self.set_cmd_auth_type( + f"iot hub generate-sas-token -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + checks=[self.exists("sas")], + ) + + # Custom duration + self.cmd( + self.set_cmd_auth_type( + f"iot hub generate-sas-token -n {LIVE_HUB} --du 1000", + auth_type=auth_phase, + ), + checks=[self.exists("sas")], + ) + + if auth_phase != "cstring": + # Custom policy + self.cmd( + self.set_cmd_auth_type( + f"iot hub generate-sas-token -n {LIVE_HUB} -g {LIVE_RG} --pn service", + auth_type=auth_phase, + ), + checks=[self.exists("sas")], + ) + + # Error - non-existent custom policy + self.cmd( + self.set_cmd_auth_type( + f"iot hub generate-sas-token --pn somepolicy -n {LIVE_HUB} -g {LIVE_RG}", + auth_type=auth_phase, + ), + expect_failure=True, + ) + + # Error - Unable to change key type when using cstring + self.cmd( + f"iot hub generate-sas-token --login {self.connection_string} --kt secondary", + expect_failure=True, + ) + + def test_iothub_connection_string_show(self): + conn_str_pattern = r"^HostName={0}.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=".format( + LIVE_HUB + ) + conn_str_eventhub_pattern = ( + r"^Endpoint=sb://(.+?)servicebus.windows.net/;SharedAccessKeyName=" + r"iothubowner;SharedAccessKey=(.+?);EntityPath=" + ) + + default_policy = "iothubowner" + nonexistent_policy = "badpolicy" + + hubs_in_sub = self.cmd("iot hub connection-string show").get_output_in_json() + + hubs_in_rg = self.cmd(f"iot hub connection-string show -g {LIVE_RG}").get_output_in_json() + assert len(hubs_in_sub) >= len(hubs_in_rg) + + self.cmd( + f"iot hub connection-string show -n {LIVE_HUB}", + checks=[self.check_pattern("connectionString", conn_str_pattern)], + ) + + self.cmd( + f"iot hub connection-string show -n {LIVE_HUB} --pn {default_policy}", + checks=[self.check_pattern("connectionString", conn_str_pattern)], + ) + + self.cmd( + f"iot hub connection-string show -n {LIVE_HUB} -g {LIVE_RG} --pn {nonexistent_policy}", + expect_failure=True, + ) + + self.cmd( + f"iot hub connection-string show --pn {nonexistent_policy}", + checks=[self.check("length(@)", 0)], + ) + + self.cmd( + f"iot hub connection-string show -n {LIVE_HUB} --eh", + checks=[self.check_pattern("connectionString", conn_str_eventhub_pattern)], + ) + + self.cmd( + f"iot hub connection-string show -n {LIVE_HUB} -g {LIVE_RG}", + checks=[ + self.check("length(@)", 1), + self.check_pattern("connectionString", conn_str_pattern), + ], + ) + + self.cmd( + f"iot hub connection-string show -n {LIVE_HUB} -g {LIVE_RG} --all", + checks=[ + self.greater_than("length(connectionString[*])", 0), + self.check_pattern("connectionString[0]", conn_str_pattern), + ], + ) + + self.cmd( + f"iot hub connection-string show -n {LIVE_HUB} -g {LIVE_RG} --all --eh", + checks=[ + self.greater_than("length(connectionString[*])", 0), + self.check_pattern( + "connectionString[0]", conn_str_eventhub_pattern + ), + ], + ) + + def test_iothub_init(self): + for auth_phase in DATAPLANE_AUTH_TYPES: + self.cmd( + self.set_cmd_auth_type( + f'iot hub query --hub-name {LIVE_HUB} -q "select * from devices"', + auth_type=auth_phase, + ), + checks=[self.check("length([*])", 0)], + ) + + # Test mode 2 handler + self.cmd( + 'iot hub query -q "select * from devices"', + expect_failure=True, + ) + + # Error - invalid cstring + self.cmd( + 'iot hub query -q "select * from devices" -l "Hostname=badlogin;key=1235"', + expect_failure=True, + ) diff --git a/azext_iot/tests/utility/test_monitor_parsers_unit.py b/azext_iot/tests/utility/test_monitor_parsers_unit.py index 3d1622181..e12e43623 100644 --- a/azext_iot/tests/utility/test_monitor_parsers_unit.py +++ b/azext_iot/tests/utility/test_monitor_parsers_unit.py @@ -9,12 +9,11 @@ import pytest from uamqp.message import Message, MessageProperties -from azext_iot.central.providers import ( - CentralDeviceProvider, - CentralDeviceTemplateProvider, +from azext_iot.central.providers.v1 import ( + CentralDeviceProviderV1, + CentralDeviceTemplateProviderV1, ) -from azext_iot.central.models.template import Template -from azext_iot.central.models.device import Device +from azext_iot.central import models as central_models from azext_iot.monitor.parsers import common_parser, central_parser from azext_iot.monitor.parsers import strings from azext_iot.monitor.models.arguments import CommonParserArguments @@ -440,7 +439,7 @@ def test_validate_against_no_component_template_should_fail(self): def test_validate_against_invalid_component_template_should_fail(self): # setup - device_template = Template( + device_template = central_models.TemplateV1( load_json(FileNames.central_property_validation_template_file) ) @@ -487,7 +486,7 @@ def test_validate_against_invalid_component_template_should_fail(self): def test_validate_invalid_telmetry_component_template_should_fail(self): # setup - device_template = Template( + device_template = central_models.TemplateV1( load_json(FileNames.central_property_validation_template_file) ) @@ -553,7 +552,7 @@ def test_validate_against_bad_template_should_not_throw(self): ) # haven't found a better way to force the error to occur within parser - parser._central_template_provider.get_device_template = lambda x: Template( + parser._central_template_provider.get_device_template = lambda x: central_models.TemplateV1( device_template ) @@ -605,14 +604,21 @@ def test_type_mismatch_should_error(self): _validate_issues(parser, Severity.error, 1, 1, [expected_details]) def _get_template(self): - return Template(load_json(FileNames.central_device_template_file)) + return central_models.TemplateV1( + load_json(FileNames.central_device_template_file) + ) def _create_parser( - self, device_template: Template, message: Message, args: CommonParserArguments + self, + device_template: central_models.TemplateV1, + message: Message, + args: CommonParserArguments, ): - device_provider = CentralDeviceProvider(cmd=None, app_id=None) - template_provider = CentralDeviceTemplateProvider(cmd=None, app_id=None) - device_provider.get_device = mock.MagicMock(return_value=Device({})) + device_provider = CentralDeviceProviderV1(cmd=None, app_id=None) + template_provider = CentralDeviceTemplateProviderV1(cmd=None, app_id=None) + device_provider.get_device = mock.MagicMock( + return_value=central_models.DeviceV1({}) + ) template_provider.get_device_template = mock.MagicMock( return_value=device_template ) diff --git a/dev_requirements b/dev_requirements index 3482a34f5..407530160 100644 --- a/dev_requirements +++ b/dev_requirements @@ -3,9 +3,9 @@ pytest-mock pytest-cov pytest-env uamqp~=1.2 -mock;python_version<'3.3' responses -black;python_version>='3.6' +urllib3[secure]>=1.21.1,<=1.25 +black wheel==0.30.0 pre-commit pylint diff --git a/linter_exclusions.yml b/linter_exclusions.yml index 83b77abf8..684bd71b9 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -13,3 +13,38 @@ dt endpoint create eventhub: eventhub_resource_group: rule_exclusions: - parameter_should_not_end_in_resource_group +iot hub configuration update: + parameters: + auth_type_dataplane: + rule_exclusions: + - no_parameter_defaults_for_update_commands +iot hub device-identity update: + parameters: + auth_type_dataplane: + rule_exclusions: + - no_parameter_defaults_for_update_commands +iot hub device-twin update: + parameters: + auth_type_dataplane: + rule_exclusions: + - no_parameter_defaults_for_update_commands +iot hub distributed-tracing update: + parameters: + auth_type_dataplane: + rule_exclusions: + - no_parameter_defaults_for_update_commands +iot hub module-identity update: + parameters: + auth_type_dataplane: + rule_exclusions: + - no_parameter_defaults_for_update_commands +iot hub module-twin update: + parameters: + auth_type_dataplane: + rule_exclusions: + - no_parameter_defaults_for_update_commands +iot edge deployment update: + parameters: + auth_type_dataplane: + rule_exclusions: + - no_parameter_defaults_for_update_commands diff --git a/pytest.ini.example b/pytest.ini.example index ede7b5ff2..6118a600e 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -2,7 +2,6 @@ junit_family = xunit1 addopts = -v - -p no:warnings norecursedirs = dist