From d1e6e4ea91a23140f5455a6110d9d54b7bad620d Mon Sep 17 00:00:00 2001 From: Rogelio Delgadillo <35089121+r-delgadillo@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:38:04 -0700 Subject: [PATCH 001/179] Revert "hide iotcentral (#149)" (#150) This reverts commit cbd60c46ab589dfdc2409903828b7292dea0a8f0. --- azext_iot/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index a03f99080..079b47baf 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -119,7 +119,7 @@ def load_command_table(self, _): cmd_group.command('delete', 'iot_dps_registration_delete') with self.command_group('iotcentral', command_type=iotcentral_ops, - deprecate_info=self.deprecate(redirect='iot central', hide=True)) as cmd_group: + deprecate_info=self.deprecate(redirect='iot central app')) as cmd_group: pass with self.command_group('iotcentral app', command_type=iotcentral_ops) as cmd_group: From db9d386ac8beef010aebfae468271b6746a56c9a Mon Sep 17 00:00:00 2001 From: Rogelio Delgadillo <35089121+r-delgadillo@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:57:00 -0700 Subject: [PATCH 002/179] Revert "Deprecate 'iotcentral' and being using 'iot central' (#146)" (#151) This reverts commit 69d9da6fcd6adc99a57d7deb9d81c12a2185078d. --- azext_iot/commands.py | 10 ++++------ azext_iot/constants.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 079b47baf..0898e2d1b 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -118,15 +118,13 @@ def load_command_table(self, _): cmd_group.command('show', 'iot_dps_registration_get') cmd_group.command('delete', 'iot_dps_registration_delete') - with self.command_group('iotcentral', command_type=iotcentral_ops, - deprecate_info=self.deprecate(redirect='iot central app')) as cmd_group: - pass - with self.command_group('iotcentral app', command_type=iotcentral_ops) as cmd_group: - cmd_group.command('monitor-events', 'iot_central_monitor_events') + cmd_group.command('monitor-events', 'iot_central_monitor_events', + deprecate_info='az iot central app monitor-events') with self.command_group('iotcentral device-twin', command_type=iotcentral_ops) as cmd_group: - cmd_group.command('show', 'iot_central_device_show') + cmd_group.command('show', 'iot_central_device_show', + deprecate_info='az iot central device-twin') with self.command_group('iot central app', command_type=iotcentral_ops) as cmd_group: cmd_group.command('monitor-events', 'iot_central_monitor_events') diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 4054eddc6..f70a4da2d 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.2" +VERSION = "0.9.1" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" From 454127ca9f976a7e04ad98985cfd614e958e286e Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 2 Apr 2020 16:08:13 -0700 Subject: [PATCH 003/179] Remove Py2.7 runtime from CI pipeline (#152) * Remove Py2.7 runtime from CI pipeline - update to merge.yml --- .azure-devops/merge.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index d1edf8a03..484ac9b11 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -69,8 +69,6 @@ jobs: vmImage: 'Ubuntu-16.04' strategy: matrix: - Python27: - python.version: '2.x' Python36: python.version: '3.6.x' Python37: From 24428b9faa49daa11e24d002ee89892f12be4c71 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Fri, 3 Apr 2020 12:16:00 -0700 Subject: [PATCH 004/179] Update CONTRIBUTING.md (#153) * Updated contribution doc with latest instructions. --- .gitignore | 5 +- CONTRIBUTING.md | 226 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 167 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index 27c48d6c7..bc8f3654c 100644 --- a/.gitignore +++ b/.gitignore @@ -312,10 +312,7 @@ src/build /doc/_build /doc/sphinx/_build /.vs/config/applicationhost.config -.vscode/settings.json -.vscode/.ropeproject/ -.vscode/tags -.vscode/cSpell.json +.vscode/ .project .pydevproject diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 940afb227..2e243fd77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,98 +1,102 @@ # Contributing -## Development Machine Setup +## Dev Setup -1. Setup Azure CLI Development Environment +1. Get Python 3: https://www.python.org/downloads/ - - Follow the [Azure CLI: Setting up your development environment](https://github.com/Azure/azure-cli/blob/master/doc/configuring_your_machine.md) steps to setup your machine for general Azure CLI development. - - Move on to the next step when you can successfully run `azdev` and see the help message. +#### Required Repositories - > Make sure you keep the virtual environment you created above activated while completing following steps. +You must fork the repositories below. Follow the videos found [here](https://github.com/Azure/azure-cli-dev-tools#setting-up-your-development-environment). -1. Update AZURE_EXTENSION_DIR and PYTHONPATH Environment Variables +1. https://github.com/Azure/azure-cli - By default, CLI extensions are installed to the `~/.azure/cliextensions` directory. For extension development, you'll want to update the AZURE_EXTENSION_DIR environment variable to `~/.azure/devcliextensions`, so your development extensions don't collide with your production extensions. +2. https://github.com/Azure/azure-iot-cli-extension - You will also need to add both the project root and and devcliextensions directory to PYTHONPATH. +> IMPORTANT: When cloning the repositories and environments, ensure they are all siblings to each other. This makes things much easier down the line. - 1. Navigate to the root of this repo on your local machine: +``` +source-directory/ +|-- azure-cli/ +|-- azure-iot-cli-extension/ +|-- .env3/ +``` - ``` - cd << clone root >> - ``` +> IMPORTANT: Ensure you keep the Python virtual environment you created above. It is required for development. - 1. Run the following script to set the environment variables. +After following the videos, ensure you have: - **Windows:** +1. Python virtual environment - ``` - set EXTENSION_PATH=%USERPROFILE%\.azure\devcliextensions\ - mkdir %EXTENSION_PATH% - set AZURE_EXTENSION_DIR=%EXTENSION_PATH% - set PYTHONPATH=%PYTHONPATH%;%EXTENSION_PATH%azure-iot;%CD% - ``` - **Linux:** +2. Functional development az cli - ``` - export EXTENSION_PATH=~/.azure/devcliextensions/ - mkdir -p $EXTENSION_PATH - echo $"export AZURE_EXTENSION_DIR=$EXTENSION_PATH" >> ~/.bash_profile - echo $"export PYTHONPATH=$PYTHONPATH:${EXTENSION_PATH}azure-iot:$(pwd)" >> ~/.bash_profile - ``` +#### Environment Variables -1. Install Extension +It is recommended that you set the following environment variables in a way such that they are persisted through machine restarts. - 1. Navigate to the root of this repo on your local machine: +You can run this setup in `bash` or `cmd` environments, this documentation just show the `powershell` flavor. - ``` - cd << clone root >> +1. Set `PYTHONPATH` to the following. Order matters here so be careful. + + ```powershell + $env:PYTHONPATH="path/to/source/azure-iot-cli-extension;path/to/source/azure-cli;path/to/source/env3/Scripts" ``` - 1. Install the Extension +2. Create a directory for your development extensions to live in - **Windows:** - ``` - pip install -U --target %AZURE_EXTENSION_DIR%/azure-iot . + ```powershell + mkdir path/to/source/extensions/azure-iot ``` - **Linux:** - ``` - pip install -U --target $AZURE_EXTENSION_DIR/azure-iot . +3. Set `AZURE_EXTENSION_DIR` to the following + + ```powershell + $env:AZURE_EXTENSION_DIR="path/to/source/extensions" ``` -1. Verify Setup +Restart any PowerShell windows you may have open and reactivate your python environment. Check that the environment variables created above have persisted. - Run the following command to view installed extensions: +#### azdev Steps - `az --debug` +Similar to the video, just execute the following command. - That will output which directory is being used to load extensions and it will show that the `azure-iot` extension has been loaded. +```powershell +azdev setup -c -r path/to/source/azure-iot-cli-extension +``` - ``` - Extensions directory: '...\.azure\devcliextensions\' - Found 1 extensions: ['azure-iot'] - ``` +#### Install dev extension -Please use `az --debug` if you run into any issues, or file an issue in this GitHub repo. +1. Change directories -Please refer to the [Azure CLI Extension Guide](https://github.com/Azure/azure-cli/tree/master/doc/extensions) for further information and help developing extensions. + ```powershell + cd path/to/source/azure-iot-cli-extension + ``` -### Running Tests +2. Install the extension (should only be needed once) -1. Install Dependencies + ```powershell + pip install -U --target path/to/source/extensions/azure-iot . + ``` - This project utilizes the following: [pytest](https://docs.pytest.org/en/latest/), [unittest](https://docs.python.org/3.6/library/unittest.html), `pytest-mock`, and `pytest-cov`. +#### Verify environment is setup correctly - Run the following to install them: +Run a command that is present in the iot extension space - `pip install -r dev_requirements` +```powershell +az iot central app -h +``` -1. Activate Virtual Environment +If this works, then you should now be able to make changes to the extension and have them reflected immediately in your az cli. - Ensure that the virtual environment you created while setting up your machine for general Azure CLI development is activated and the dev_setup.py script has been run. +## Unit and Integration Testing #### Unit Tests +You may need to install the dev_requirements for this + +```powershell +pip install -r path/to/source/dev_requirements +``` + _Hub:_ `pytest azext_iot/tests/test_iot_ext_unit.py` @@ -106,10 +110,14 @@ Integration tests are run against Azure resources and depend on environment vari ##### Azure Resource Setup 1. Create IoT Hub -> IMPORTANT: Your IoT Hub must be created specifically for integration tests and must not contain any devices when the tests are run. -1. Create Files Storage - In IoT Hub, click Files, create a new Storage Account and link to an empty Container. -1. Create IoT Hub Device Provisioning Service (DPS) -1. Link IoT Hub to DPS - From DPS, click "Linked IoT Hub" and link the IoT Hub you just created. + + > IMPORTANT: Your IoT Hub must be created specifically for integration tests and must not contain any devices when the tests are run. + +2. Create Files Storage - In IoT Hub, click Files, create a new Storage Account and link to an empty Container. + +3. Create IoT Hub Device Provisioning Service (DPS) + +4. Link IoT Hub to DPS - From DPS, click "Linked IoT Hub" and link the IoT Hub you just created. ##### Environment Variables You can either manually set the environment variables or use the `pytest.ini.example` file in the root of the extension repo. To use that file, rename it to `pytest.ini`, open it and set the variables as indicated below. @@ -145,7 +153,105 @@ Execute the following command to run both Unit and Integration tests and output `pytest -v . --cov=azext_iot --cov-config .coveragerc` -#### Microsoft CLA +#### Formatting and Linting + +We use our flake8 and pylint rules. We recommend you set up your IDE as per the VSCode setup below for best compliance. + +We are also starting to use `python black`. To set this up on VSCode, see the following blog post + +https://medium.com/@marcobelo/setting-up-python-black-on-visual-studio-code-5318eba4cd00 + +## Optional + +#### VSCode setup + +1. Install VSCode + +2. Install the required extensions + * ([ms-python.python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) is recommended) + +3. Set up `settings.json` + + ```json + { + "python.pythonPath": "path/to/source/env3/Scripts/python.exe", + "python.venvPath": "path/to/source/", + "python.linting.pylintEnabled": true, + "python.autoComplete.extraPaths": [ + "path/to/source/env3/Lib/site-packages" + ], + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": [ + "--config=setup.cfg" + ], + "files.associations": { + "*/.azure-devops/.yml": "azure-pipelines" + } + } + ``` + +4. Set up `launch.json` + + ```json + { + "version": "0.2.0", + "configurations": [ + { + "name": "Azure CLI Debug (Integrated Console)", + "type": "python", + "request": "launch", + "pythonPath": "${config:python.pythonPath}", + "program": "${workspaceRoot}/../azure-cli/src/azure-cli/azure/cli/__main__.py", + "cwd": "${workspaceRoot}", + "args": [ + "--help" + ], + "console": "integratedTerminal", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ], + "justMyCode": false + } + ] + } + ``` + + * launch.json was derived from [this](https://raw.githubusercontent.com/Azure/azure-cli/dev/.vscode/launch.json) file + + * Note: your "program" path might be different if you did not set up the folder structure as siblings as recommended above + + * Note: when passing args, ensure they are all comma separated. + + Correct: + ``` + "args": [ + "--a", "value", "--b", "value" + ], + ``` + + Incorrect: + ``` + "args": [ + "--a value --b value" + ], + ``` + +5. Set up python black. + +6. You should now be able to place breakpoints in VSCode and see execution halt as the code hits them. + +### Python debugging + +https://docs.python.org/3/library/pdb.html + + +1. `pip install pdb` +2. If you need a breakpoint, put `import pdb; pdb.set_trace()` in your code +3. Run your command, it should break execution wherever you put the breakpoint. + +# Microsoft CLA This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us From 18d28728bab81c9f07d3dd255fc8855c90371367 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 3 Apr 2020 12:33:33 -0700 Subject: [PATCH 005/179] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..4a40b52a3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at iotupx@microsoft.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq From 9da5bd5914f9a09f52e23178472f3b2761eced91 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 3 Apr 2020 12:42:05 -0700 Subject: [PATCH 006/179] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..6b81c6589 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment (please complete the following information):** + - OS: [e.g. Windows 10, Ubuntu 16.04, MacOS X] + - Shell: [e.g. bash, wsl bash, powershell, cmd] + - Az CLI version: [e.g. 2.0.80] + - IoT extension version: [e.g. 0.9.1] + - Python version (if pip installed): [e.g. 3.8.1] + +**Additional context** +Add any other context about the problem here. From 44ad3eb116331a970dff6c827e8ec200fb3f9ba6 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 3 Apr 2020 12:42:36 -0700 Subject: [PATCH 007/179] Delete ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 1c6b96da8..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - - -Thank you for submitting an issue! - -We want you to have an overall smooth dev experience. In order to best help resolve the issue, please provide as much detail as possible. - -Details like Python version, OS version, Shell Type (e.g. bash, cmd.exe, Bash on Windows), CLI core (`az --version`) and IoT Extension (`az extension list`) versions are key in building context around the issue. - ---- From a0c7d06e3efa779a5f7b744e601c0d01cdc043c4 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 3 Apr 2020 18:15:29 -0700 Subject: [PATCH 008/179] Update CONTRIBUTING.md Contributing tweaks. --- CONTRIBUTING.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e243fd77..b880cd1e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ #### Required Repositories -You must fork the repositories below. Follow the videos found [here](https://github.com/Azure/azure-cli-dev-tools#setting-up-your-development-environment). +You must fork and clone the repositories below. Follow the videos and instructions found [here](https://github.com/Azure/azure-cli-dev-tools#setting-up-your-development-environment). 1. https://github.com/Azure/azure-cli @@ -35,22 +35,22 @@ It is recommended that you set the following environment variables in a way such You can run this setup in `bash` or `cmd` environments, this documentation just show the `powershell` flavor. -1. Set `PYTHONPATH` to the following. Order matters here so be careful. +1. Create a directory for your development extensions to live in ```powershell - $env:PYTHONPATH="path/to/source/azure-iot-cli-extension;path/to/source/azure-cli;path/to/source/env3/Scripts" + mkdir path/to/source/extensions/azure-iot ``` -2. Create a directory for your development extensions to live in +2. Set `AZURE_EXTENSION_DIR` to the following ```powershell - mkdir path/to/source/extensions/azure-iot + $env:AZURE_EXTENSION_DIR="path/to/source/extensions" ``` -3. Set `AZURE_EXTENSION_DIR` to the following +3. Set `PYTHONPATH` to the following. Order matters here so be careful. ```powershell - $env:AZURE_EXTENSION_DIR="path/to/source/extensions" + $env:PYTHONPATH="path/to/source/azure-iot-cli-extension;path/to/source/extensions/azure-iot" ``` Restart any PowerShell windows you may have open and reactivate your python environment. Check that the environment variables created above have persisted. @@ -60,7 +60,7 @@ Restart any PowerShell windows you may have open and reactivate your python envi Similar to the video, just execute the following command. ```powershell -azdev setup -c -r path/to/source/azure-iot-cli-extension +azdev setup -c path/to/source/azure-cli ``` #### Install dev extension @@ -259,4 +259,4 @@ the rights to use your contribution. For details, visit https://cla.microsoft.co When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. \ No newline at end of file +provided by the bot. You will only need to do this once across all repos using our CLA. From 31b1295078aacd8d8b8d72ea39552a5027532eab Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Fri, 10 Apr 2020 14:38:39 -0700 Subject: [PATCH 009/179] Add support for az iot central app validate-messages command (#155) * Adds az iot central app validate-messages command, tests and help content. * Adds EventParser and unit tests * Event error simulation supported * Formatting updates via python black * Removes importlib usage (due to dropping support of Python2.7) Co-authored-by: Pranshu Bansal Co-authored-by: Raj valluri --- .gitignore | 3 +- azext_iot/_help.py | 480 ++++-- azext_iot/_params.py | 1455 ++++++++++++------- azext_iot/commands.py | 345 +++-- azext_iot/constants.py | 2 +- azext_iot/operations/central.py | 111 +- azext_iot/operations/events3/__init__.py | 4 + azext_iot/operations/events3/_events.py | 160 +- azext_iot/operations/events3/_parser.py | 259 ++++ azext_iot/operations/hub.py | 1681 ++++++++++++++++------ azext_iot/tests/test_iot_central_int.py | 60 +- azext_iot/tests/test_iot_central_unit.py | 117 +- azext_iot/tests/test_iot_utility_unit.py | 220 ++- dev_requirements | 1 + 14 files changed, 3494 insertions(+), 1404 deletions(-) create mode 100644 azext_iot/operations/events3/_parser.py diff --git a/.gitignore b/.gitignore index bc8f3654c..adc940887 100644 --- a/.gitignore +++ b/.gitignore @@ -187,7 +187,8 @@ _pkginfo.txt # Ignore Visual Studio and Pytest cache *.[Cc]ache *.pytest_cache -pytest.ini +*.ini +!pytest.ini.example # Others ClientBin/ diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 9ed667eb7..ecdd3bdd6 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -10,7 +10,9 @@ from knack.help_files import helps -helps['iot'] = """ +helps[ + "iot" +] = """ type: group short-summary: Manage Internet of Things (IoT) assets. Augmented with the IoT extension. @@ -19,12 +21,16 @@ https://github.com/Azure/azure-iot-cli-extension/wiki/Tips """ -helps['iot hub'] = """ +helps[ + "iot hub" +] = """ type: group short-summary: Manage entities in an Azure IoT Hub. """ -helps['iot hub monitor-events'] = """ +helps[ + "iot hub monitor-events" +] = """ type: command short-summary: Monitor device telemetry & messages sent to an IoT Hub. long-summary: | @@ -65,7 +71,9 @@ az iot hub monitor-events -n {iothub_name} --content-type application/json """ -helps['iot hub monitor-feedback'] = """ +helps[ + "iot hub monitor-feedback" +] = """ type: command short-summary: Monitor feedback sent by devices to acknowledge cloud-to-device (C2D) messages. long-summary: | @@ -88,12 +96,16 @@ az iot hub monitor-feedback -n {iothub_name} -d {device_id} -w {message_id} """ -helps['iot hub device-identity'] = """ +helps[ + "iot hub device-identity" +] = """ type: group short-summary: Manage IoT devices. """ -helps['iot hub device-identity create'] = """ +helps[ + "iot hub device-identity create" +] = """ type: command short-summary: Create a device in an IoT Hub. examples: @@ -129,17 +141,23 @@ --status disabled --status-reason 'for reasons' """ -helps['iot hub device-identity show'] = """ +helps[ + "iot hub device-identity show" +] = """ type: command short-summary: Get the details of an IoT Hub device. """ -helps['iot hub device-identity list'] = """ +helps[ + "iot hub device-identity list" +] = """ type: command short-summary: List devices in an IoT Hub. """ -helps['iot hub device-identity update'] = """ +helps[ + "iot hub device-identity update" +] = """ type: command short-summary: Update an IoT Hub device. long-summary: Use --set followed by property assignments for updating a device. @@ -158,31 +176,41 @@ --set status=disabled capabilities.iotEdge=true """ -helps['iot hub device-identity delete'] = """ +helps[ + "iot hub device-identity delete" +] = """ type: command short-summary: Delete an IoT Hub device. """ -helps['iot hub device-identity show-connection-string'] = """ +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 export'] = """ +helps[ + "iot hub device-identity export" +] = """ type: command short-summary: Export all device identities from an IoT Hub to an Azure Storage blob container. long-summary: For more information, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities """ -helps['iot hub device-identity import'] = """ +helps[ + "iot hub device-identity import" +] = """ type: command short-summary: Import device identities to an IoT Hub from a blob. long-summary: For more information, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities """ -helps['iot hub device-identity get-parent'] = """ +helps[ + "iot hub device-identity get-parent" +] = """ type: command short-summary: Get the parent device of the specified device. examples: @@ -191,7 +219,9 @@ az iot hub device-identity get-parent -d {non_edge_device_id} -n {iothub_name} """ -helps['iot hub device-identity set-parent'] = """ +helps[ + "iot hub device-identity set-parent" +] = """ type: command short-summary: Set the parent device of the specified non-edge device. examples: @@ -204,7 +234,9 @@ az iot hub device-identity set-parent -d {non_edge_device_id} --pd {edge_device_id} --force -n {iothub_name} """ -helps['iot hub device-identity add-children'] = """ +helps[ + "iot hub device-identity add-children" +] = """ type: command short-summary: Add specified comma-separated list of non edge device ids as children of specified edge device. examples: @@ -219,7 +251,9 @@ -n {iothub_name} -f """ -helps['iot hub device-identity list-children'] = """ +helps[ + "iot hub device-identity list-children" +] = """ type: command short-summary: Print comma-separated list of assigned child devices. examples: @@ -228,7 +262,9 @@ az iot hub device-identity list-children -d {edge_device_id} -n {iothub_name} """ -helps['iot hub device-identity remove-children'] = """ +helps[ + "iot hub device-identity remove-children" +] = """ type: command short-summary: Remove non edge devices as children from specified edge device. examples: @@ -241,17 +277,23 @@ az iot hub device-identity remove-children -d {edge_device_id} --remove-all """ -helps['iot hub device-twin'] = """ +helps[ + "iot hub device-twin" +] = """ type: group short-summary: Manage IoT device twin configuration. """ -helps['iot hub device-twin show'] = """ +helps[ + "iot hub device-twin show" +] = """ type: command short-summary: Get a device twin definition. """ -helps['iot hub device-twin update'] = """ +helps[ + "iot hub device-twin update" +] = """ type: command short-summary: Update device twin definition. long-summary: Use --set followed by property assignments for updating a device twin. @@ -267,7 +309,9 @@ --set tags.location.region='null' """ -helps['iot hub device-twin replace'] = """ +helps[ + "iot hub device-twin replace" +] = """ type: command short-summary: Replace device twin definition with target json. long-summary: Input json directly or use a file path. @@ -277,32 +321,44 @@ az iot hub device-twin replace -d {device_id} -n {iothub_name} -j ../mydevicetwin.json """ -helps['iot hub module-identity'] = """ +helps[ + "iot hub module-identity" +] = """ type: group short-summary: Manage IoT device modules. """ -helps['iot hub module-identity show-connection-string'] = """ +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'] = """ +helps[ + "iot hub module-identity create" +] = """ type: command short-summary: Create a module on a target IoT device in an IoT Hub. """ -helps['iot hub module-identity show'] = """ +helps[ + "iot hub module-identity show" +] = """ type: command short-summary: Get the details of an IoT device module in an IoT Hub. """ -helps['iot hub module-identity list'] = """ +helps[ + "iot hub module-identity list" +] = """ type: command short-summary: List modules located on an IoT device in an IoT Hub. """ -helps['iot hub module-identity update'] = """ +helps[ + "iot hub module-identity update" +] = """ type: command short-summary: Update an IoT Hub device module. long-summary: Use --set followed by property assignments for updating a module. @@ -315,22 +371,30 @@ authentication.symmetricKey.secondaryKey="" """ -helps['iot hub module-identity delete'] = """ +helps[ + "iot hub module-identity delete" +] = """ type: command short-summary: Delete a device in an IoT Hub. """ -helps['iot hub module-twin'] = """ +helps[ + "iot hub module-twin" +] = """ type: group short-summary: Manage IoT device module twin configuration. """ -helps['iot hub module-twin show'] = """ +helps[ + "iot hub module-twin show" +] = """ type: command short-summary: Show a module twin definition. """ -helps['iot hub module-twin update'] = """ +helps[ + "iot hub module-twin update" +] = """ type: command short-summary: Update module twin definition. long-summary: Use --set followed by property assignments for updating a module. @@ -346,7 +410,9 @@ properties.desired.condition.temperature.critical='null' """ -helps['iot hub module-twin replace'] = """ +helps[ + "iot hub module-twin replace" +] = """ type: command short-summary: Replace a module twin definition with target json. long-summary: Input json directly or use a file path. @@ -357,7 +423,9 @@ -m {module_name} -j ../mymodtwin.json """ -helps['iot hub generate-sas-token'] = """ +helps[ + "iot hub generate-sas-token" +] = """ type: command short-summary: Generate a SAS token for a target IoT Hub, device or module. long-summary: For device SAS tokens, the policy parameter is used to @@ -379,17 +447,23 @@ --login 'HostName=myhub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=12345' """ -helps['iot hub invoke-module-method'] = """ +helps[ + "iot hub invoke-module-method" +] = """ type: command short-summary: Invoke an Edge module method. """ -helps['iot hub invoke-device-method'] = """ +helps[ + "iot hub invoke-device-method" +] = """ type: command short-summary: Invoke a device method. """ -helps['iot hub query'] = """ +helps[ + "iot hub query" +] = """ type: command short-summary: Query an IoT Hub using a powerful SQL-like language. long-summary: Query an IoT Hub using a powerful SQL-like language to retrieve information @@ -405,12 +479,16 @@ az iot hub query -n {iothub_name} -q "select * from devices.modules where devices.deviceId = '{device_id}'" """ -helps['iot hub configuration'] = """ +helps[ + "iot hub configuration" +] = """ type: group short-summary: Manage IoT automatic device management configuration at scale. """ -helps['iot hub configuration create'] = """ +helps[ + "iot hub configuration create" +] = """ type: command short-summary: Create an IoT automatic device management configuration in a target IoT Hub. long-summary: | @@ -448,17 +526,23 @@ --metrics '{\\"metrics\\": {\\"queries\\": {\\"mymetric\\":\\"select moduleId from devices.modules where tags.location=''US''\\"}}}' """ -helps['iot hub configuration show'] = """ +helps[ + "iot hub configuration show" +] = """ type: command short-summary: Get the details of an IoT automatic device management configuration. """ -helps['iot hub configuration list'] = """ +helps[ + "iot hub configuration list" +] = """ type: command short-summary: List IoT automatic device management configurations in an IoT Hub. """ -helps['iot hub configuration update'] = """ +helps[ + "iot hub configuration update" +] = """ type: command short-summary: | Update specified properties of an IoT automatic device management configuration. @@ -474,12 +558,16 @@ targetCondition="tags.building=43 and tags.environment='dev'" """ -helps['iot hub configuration delete'] = """ +helps[ + "iot hub configuration delete" +] = """ type: command short-summary: Delete an IoT device configuration. """ -helps['iot hub configuration show-metric'] = """ +helps[ + "iot hub configuration show-metric" +] = """ type: command short-summary: Evaluate a target user or system metric defined in an IoT device configuration examples: @@ -492,12 +580,16 @@ --metric-type system """ -helps['iot hub distributed-tracing'] = """ +helps[ + "iot hub distributed-tracing" +] = """ type: group short-summary: Manage distributed settings per-device. """ -helps['iot hub distributed-tracing show'] = """ +helps[ + "iot hub distributed-tracing show" +] = """ type: command short-summary: Get the distributed tracing settings for a device. examples: @@ -506,7 +598,9 @@ az iot hub distributed-tracing show -d {device_id} -n {iothub_name} """ -helps['iot hub distributed-tracing update'] = """ +helps[ + "iot hub distributed-tracing update" +] = """ type: command short-summary: Update the distributed tracing options for a device. examples: @@ -515,37 +609,51 @@ az iot hub distributed-tracing update -d {device_id} --sm on --sr 50 -n {iothub_name} """ -helps['iot device'] = """ +helps[ + "iot device" +] = """ type: group short-summary: Leverage device-to-cloud and cloud-to-device messaging capabilities. """ -helps['iot device c2d-message'] = """ +helps[ + "iot device c2d-message" +] = """ type: group short-summary: Cloud-to-device messaging commands. """ -helps['iot device c2d-message abandon'] = """ +helps[ + "iot device c2d-message abandon" +] = """ type: command short-summary: Abandon a cloud-to-device message. """ -helps['iot device c2d-message complete'] = """ +helps[ + "iot device c2d-message complete" +] = """ type: command short-summary: Complete a cloud-to-device message. """ -helps['iot device c2d-message receive'] = """ +helps[ + "iot device c2d-message receive" +] = """ type: command short-summary: Receive a cloud-to-device message. """ -helps['iot device c2d-message reject'] = """ +helps[ + "iot device c2d-message reject" +] = """ type: command short-summary: Reject or deadletter a cloud-to-device message. """ -helps['iot device c2d-message send'] = """ +helps[ + "iot device c2d-message send" +] = """ type: command short-summary: Send a cloud-to-device message. long-summary: | @@ -564,7 +672,9 @@ az iot device c2d-message send -d {device_id} -n {iothub_name} --ack full --wait """ -helps['iot device send-d2c-message'] = """ +helps[ + "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. @@ -579,7 +689,9 @@ text: az iot device send-d2c-message -n {iothub_name} -d {device_id} --props '$.mid=;$.cid=' """ -helps['iot device simulate'] = """ +helps[ + "iot device simulate" +] = """ type: command short-summary: | Simulate a device in an Azure IoT Hub. @@ -609,12 +721,16 @@ text: az iot device simulate -n {iothub_name} -d {device_id} --rs abandon --protocol http """ -helps['iot device upload-file'] = """ +helps[ + "iot device upload-file" +] = """ type: command short-summary: Upload a local file as a device to a pre-configured blob storage container. """ -helps['iot edge'] = """ +helps[ + "iot edge" +] = """ type: group short-summary: Manage IoT solutions on the Edge. long-summmary: | @@ -627,7 +743,9 @@ https://docs.microsoft.com/en-us/azure/iot-edge/ """ -helps['iot edge set-modules'] = """ +helps[ + "iot edge set-modules" +] = """ type: command short-summary: Set edge modules on a single device. long-summary: | @@ -640,12 +758,16 @@ az iot edge set-modules --hub-name {iothub_name} --device-id {device_id} --content ../modules_content.json """ -helps['iot edge deployment'] = """ +helps[ + "iot edge deployment" +] = """ type: group short-summary: Manage IoT Edge deployments at scale. """ -helps['iot edge deployment create'] = """ +helps[ + "iot edge deployment create" +] = """ type: command short-summary: Create an IoT Edge deployment in a target IoT Hub. long-summary: | @@ -681,17 +803,23 @@ --layered """ -helps['iot edge deployment show'] = """ +helps[ + "iot edge deployment show" +] = """ type: command short-summary: Get the details of an IoT Edge deployment. """ -helps['iot edge deployment list'] = """ +helps[ + "iot edge deployment list" +] = """ type: command short-summary: List IoT Edge deployments in an IoT Hub. """ -helps['iot edge deployment update'] = """ +helps[ + "iot edge deployment update" +] = """ type: command short-summary: | Update specified properties of an IoT Edge deployment. @@ -707,12 +835,16 @@ --set labels='{"purpose":"dev", "owners":"IoTEngineering"}' targetCondition='tags.building=9' """ -helps['iot edge deployment delete'] = """ +helps[ + "iot edge deployment delete" +] = """ type: command short-summary: Delete an IoT Edge deployment. """ -helps['iot edge deployment show-metric'] = """ +helps[ + "iot edge deployment show-metric" +] = """ type: command short-summary: Evaluate a target system metric defined in an IoT Edge deployment. examples: @@ -721,28 +853,38 @@ az iot edge deployment show-metric -m appliedCount -d {deployment_name} -n {iothub_name} """ -helps['iot dps'] = """ +helps[ + "iot dps" +] = """ type: group short-summary: Manage entities in an Azure IoT Hub Device Provisioning Service. Augmented with the IoT extension. """ -helps['iot dps enrollment'] = """ +helps[ + "iot dps enrollment" +] = """ type: group short-summary: Manage enrollments in an Azure IoT Hub Device Provisioning Service. """ -helps['iot dps enrollment list'] = """ +helps[ + "iot dps enrollment list" +] = """ type: command short-summary: List device enrollments in an Azure IoT Hub Device Provisioning Service. """ -helps['iot dps enrollment show'] = """ +helps[ + "iot dps enrollment show" +] = """ type: command short-summary: Get device enrollment details in an Azure IoT Hub Device Provisioning Service. """ -helps['iot dps enrollment create'] = """ +helps[ + "iot dps enrollment create" +] = """ type: command short-summary: Create a device enrollment in an Azure IoT Hub Device Provisioning Service. examples: @@ -795,7 +937,9 @@ --endorsement-key 14963E8F3BA5B3984110B3C1CA8E8B89 --iot-hubs "{iot_hub_host_name1} {iot_hub_host_name2}" """ -helps['iot dps enrollment update'] = """ +helps[ + "iot dps enrollment update" +] = """ type: command short-summary: Update a device enrollment in an Azure IoT Hub Device Provisioning Service. examples: @@ -831,27 +975,37 @@ --etag AAAAAAAAAAA= --iot-hubs "{iot_hub_host_name1} {iot_hub_host_name2} {iot_hub_host_name3}" """ -helps['iot dps enrollment delete'] = """ +helps[ + "iot dps enrollment delete" +] = """ type: command short-summary: Delete a device enrollment in an Azure IoT Hub Device Provisioning Service. """ -helps['iot dps enrollment-group'] = """ +helps[ + "iot dps enrollment-group" +] = """ type: group short-summary: Manage Azure IoT Hub Device Provisioning Service. """ -helps['iot dps enrollment-group list'] = """ +helps[ + "iot dps enrollment-group list" +] = """ type: command short-summary: List enrollments groups in an Azure IoT Hub Device Provisioning Service. """ -helps['iot dps enrollment-group show'] = """ +helps[ + "iot dps enrollment-group show" +] = """ type: command short-summary: Get the details of an enrollment group in an Azure IoT Hub Device Provisioning Service. """ -helps['iot dps enrollment-group create'] = """ +helps[ + "iot dps enrollment-group create" +] = """ type: command short-summary: Create an enrollment group in an Azure IoT Hub Device Provisioning Service. examples: @@ -883,7 +1037,9 @@ """ -helps['iot dps enrollment-group update'] = """ +helps[ + "iot dps enrollment-group update" +] = """ type: command short-summary: Update an enrollment group in an Azure IoT Hub Device Provisioning Service. examples: @@ -913,33 +1069,45 @@ --enrollment-id {enrollment_id} --primary-key {new_primary_key} --etag AAAAAAAAAAA= """ -helps['iot dps enrollment-group delete'] = """ +helps[ + "iot dps enrollment-group delete" +] = """ type: command short-summary: Delete an enrollment group in an Azure IoT Hub Device Provisioning Service. """ -helps['iot dps registration'] = """ +helps[ + "iot dps registration" +] = """ type: group short-summary: Manage Azure IoT Hub Device Provisioning Service registrations. """ -helps['iot dps registration list'] = """ +helps[ + "iot dps registration list" +] = """ type: command short-summary: List device registration state in an Azure IoT Hub Device Provisioning Service enrollment group. """ -helps['iot dps registration show'] = """ +helps[ + "iot dps registration show" +] = """ type: command short-summary: Get the device registration state in an Azure IoT Hub Device Provisioning Service. """ -helps['iot dps registration delete'] = """ +helps[ + "iot dps registration delete" +] = """ type: command short-summary: Delete a device registration in an Azure IoT Hub Device Provisioning Service. """ -helps['iotcentral app monitor-events'] = """ +helps[ + "iotcentral app monitor-events" +] = """ type: command short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. long-summary: | @@ -975,29 +1143,39 @@ az iotcentral app monitor-events --app-id {app_id} --output json """ -helps['iotcentral device-twin'] = """ +helps[ + "iotcentral device-twin" +] = """ type: group short-summary: Manage IoT Central device twins. long-summary: DEPRECATED. Use 'az iot central device-twin' instead. """ -helps['iotcentral device-twin show'] = """ +helps[ + "iotcentral device-twin show" +] = """ type: command short-summary: Get the device twin from IoT Hub. long-summary: DEPRECATED. Use 'az iot central device-twin show' instead. """ -helps['iot central'] = """ +helps[ + "iot central" +] = """ type: group short-summary: Manage Azure IoT Central assets. """ -helps['iot central app'] = """ +helps[ + "iot central app" +] = """ type: group short-summary: Manage Azure IoT Central applications. """ -helps['iot central app monitor-events'] = """ +helps[ + "iot central app monitor-events" +] = """ type: command short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. long-summary: | @@ -1031,22 +1209,58 @@ az iot central app monitor-events --app-id {app_id} --output json """ -helps['iot central device-twin'] = """ + +helps[ + "iot central app validate-messages" +] = """ + type: command + short-summary: Validate messages sent to the IoT Hub for an IoT Central app. + long-summary: | + EXPERIMENTAL requires Python 3.5+ + This command relies on and may install dependent Cython package (uamqp) upon first execution. + https://github.com/Azure/azure-uamqp-python + examples: + - name: Basic usage + text: > + az iot central app validate-messages --app-id {app_id} + - name: Basic usage when filtering on target device + text: > + az iot central app validate-messages --app-id {app_id} -d {device_id} + - name: Basic usage when filtering targeted devices with a wildcard in the ID + text: > + az iot central app validate-messages --app-id {app_id} -d Device* + - name: Filter device and specify an Event Hub consumer group to bind to. + text: > + az iot central app validate-messages --app-id {app_id} -d {device_id} --cg {consumer_group_name} + - name: Will randomly convert a recieved message into an error so you can see what sample errors might look like. + text: > + az iot central app validate-messages --app-id {app_id} --simulate-errors + """ + +helps[ + "iot central device-twin" +] = """ type: group short-summary: Manage IoT Central device twins. """ -helps['iot central device-twin show'] = """ +helps[ + "iot central device-twin show" +] = """ type: command short-summary: Get the device twin from IoT Hub. """ -helps['iot dt'] = """ +helps[ + "iot dt" +] = """ type: group short-summary: Manage digital twin of an IoT Plug and Play device. """ -helps['iot dt invoke-command'] = """ +helps[ + "iot dt invoke-command" +] = """ type: command short-summary: Executes a command on an IoT Plug and Play device. long-summary: You can leverage az login and provide --hub-name instead of --login for every command. @@ -1063,7 +1277,9 @@ --command-name {command_name} --command-payload {payload} """ -helps['iot dt list-interfaces'] = """ +helps[ + "iot dt list-interfaces" +] = """ type: command short-summary: List interfaces of a target IoT Plug and Play device. long-summary: You can leverage az login and provide --hub-name instead of --login for every command. @@ -1077,7 +1293,9 @@ az iot dt list-interfaces --hub-name {iothub_name} --device-id {device_id} """ -helps['iot dt list-properties'] = """ +helps[ + "iot dt list-properties" +] = """ type: command short-summary: List properties of a target IoT Plug and Play device interface(s). long-summary: You can leverage az login and provide --hub-name instead of --login for every command. @@ -1095,7 +1313,9 @@ --device-id {device_id} --interface {plug_and_play_interface} """ -helps['iot dt list-commands'] = """ +helps[ + "iot dt list-commands" +] = """ type: command short-summary: List commands of an IoT Plug and Play devices interface(s). long-summary: You can leverage az login and provide --hub-name instead of --login for every command. @@ -1119,7 +1339,9 @@ --device-id {device_id} --interface {plug_and_play_interface} """ -helps['iot dt monitor-events'] = """ +helps[ + "iot dt monitor-events" +] = """ type: command short-summary: Monitor Digital Twin events. long-summary: You can leverage az login and provide --hub-name instead of --login for every command. @@ -1143,7 +1365,9 @@ --interface {plug_and_play_interface} --consumer-group {consumer_group_name} --properties all """ -helps['iot dt update-property'] = """ +helps[ + "iot dt update-property" +] = """ type: command short-summary: Update an IoT Plug and Play device interfaces writable property. examples: @@ -1157,17 +1381,23 @@ --interface-payload {payload} """ -helps['iot pnp'] = """ +helps[ + "iot pnp" +] = """ type: group short-summary: Manage entities of an IoT Plug and Play model repository. """ -helps['iot pnp interface'] = """ +helps[ + "iot pnp interface" +] = """ type: group short-summary: Manage interfaces in an IoT Plug and Play model repository. """ -helps['iot pnp interface publish'] = """ +helps[ + "iot pnp interface publish" +] = """ type: command short-summary: Publish an interface to public repository. examples: @@ -1176,7 +1406,9 @@ az iot pnp interface publish -r {pnp_repository} --interface {plug_and_play_interface_id} """ -helps['iot pnp interface create'] = """ +helps[ + "iot pnp interface create" +] = """ type: command short-summary: Create an interface in the company repository. examples: @@ -1185,7 +1417,9 @@ az iot pnp interface create --def {plug_and_play_interface_file_path} -r {pnp_repository} """ -helps['iot pnp interface update'] = """ +helps[ + "iot pnp interface update" +] = """ type: command short-summary: Update an interface in the company repository. examples: @@ -1194,7 +1428,9 @@ az iot pnp interface update --def {updated_plug_and_play_interface_file_path} -r {pnp_repository} """ -helps['iot pnp interface list'] = """ +helps[ + "iot pnp interface list" +] = """ type: command short-summary: List all interfaces. examples: @@ -1206,7 +1442,9 @@ az iot pnp interface list """ -helps['iot pnp interface show'] = """ +helps[ + "iot pnp interface show" +] = """ type: command short-summary: Get the details of an interface. examples: @@ -1218,7 +1456,9 @@ az iot pnp interface show --interface {plug_and_play_interface_id} """ -helps['iot pnp interface delete'] = """ +helps[ + "iot pnp interface delete" +] = """ type: command short-summary: Delete an interface in the company repository. examples: @@ -1227,12 +1467,16 @@ az iot pnp interface delete -r {pnp_repository} --interface {plug_and_play_interface_id} """ -helps['iot pnp capability-model'] = """ +helps[ + "iot pnp capability-model" +] = """ type: group short-summary: Manage device capability models in an IoT Plug and Play model repository. """ -helps['iot pnp capability-model list'] = """ +helps[ + "iot pnp capability-model list" +] = """ type: command short-summary: List all capability-model. examples: @@ -1244,7 +1488,9 @@ az iot pnp capability-model list """ -helps['iot pnp capability-model show'] = """ +helps[ + "iot pnp capability-model show" +] = """ type: command short-summary: Get the details of a capability-model. examples: @@ -1256,7 +1502,9 @@ az iot pnp capability-model show --model {plug_and_play_capability_model_id} """ -helps['iot pnp capability-model create'] = """ +helps[ + "iot pnp capability-model create" +] = """ type: command short-summary: Create a capability-model in the company repository. examples: @@ -1265,7 +1513,9 @@ az iot pnp capability-model create --def {plug_and_play_capability_model_file_path} -r {pnp_repository} """ -helps['iot pnp capability-model publish'] = """ +helps[ + "iot pnp capability-model publish" +] = """ type: command short-summary: Publish the capability-model to public repository. examples: @@ -1275,7 +1525,9 @@ --model {plug_and_play_capability_model_id} """ -helps['iot pnp capability-model delete'] = """ +helps[ + "iot pnp capability-model delete" +] = """ type: command short-summary: Delete the capability-model in the company repository. examples: @@ -1285,7 +1537,9 @@ --model {plug_and_play_capability_model_id} """ -helps['iot pnp capability-model update'] = """ +helps[ + "iot pnp capability-model update" +] = """ type: command short-summary: Update the capability-model in the company repository. examples: diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 10ca49be1..322b2b2bb 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -13,7 +13,7 @@ resource_group_name_type, get_enum_type, get_resource_name_completion_list, - get_three_state_flag + get_three_state_flag, ) from azext_iot.common.shared import ( EntityStatusType, @@ -30,38 +30,39 @@ ModelSourceType, JobType, JobCreateType, - JobStatusType + JobStatusType, ) from azext_iot._validators import mode2_iot_login_handler from azext_iot.assets.user_messages import info_param_properties_device hub_name_type = CLIArgumentType( - completer=get_resource_name_completion_list('Microsoft.Devices/IotHubs'), - help='IoT Hub name.') + completer=get_resource_name_completion_list("Microsoft.Devices/IotHubs"), + help="IoT Hub name.", +) event_msg_prop_type = CLIArgumentType( - options_list=['--properties', '--props', '-p'], - nargs='*', - choices=CaseInsensitiveList(['sys', 'app', 'anno', 'all']), - help='Indicate key message properties to output. ' - 'sys = system properties, app = application properties, anno = annotations' + options_list=["--properties", "--props", "-p"], + nargs="*", + choices=CaseInsensitiveList(["sys", "app", "anno", "all"]), + help="Indicate key message properties to output. " + "sys = system properties, app = application properties, anno = annotations", ) # There is a bug in CLI core preventing treating --qos as an integer. # Until its resolved, ensure casting of value to integer # TODO: azure.cli.core.parser line 180 difflib.get_close_matches qos_type = CLIArgumentType( - options_list=['--qos'], + options_list=["--qos"], type=str, nargs="?", choices=["0", "1"], - help='Quality of Service. 0 = At most once, 1 = At least once. 2 (Exactly once) is not supported.' + help="Quality of Service. 0 = At most once, 1 = At least once. 2 (Exactly once) is not supported.", ) event_timeout_type = CLIArgumentType( - options_list=['--timeout', '--to', '-t'], + options_list=["--timeout", "--to", "-t"], type=int, - help='Maximum seconds to maintain connection without receiving message. Use 0 for infinity. ' + help="Maximum seconds to maintain connection without receiving message. Use 0 for infinity. ", ) @@ -69,492 +70,946 @@ def load_arguments(self, _): """ Load CLI Args for Knack parser """ - with self.argument_context('iot') as context: - context.argument('login', options_list=['--login', '-l'], - validator=mode2_iot_login_handler, - help='This command supports an entity connection string with rights to perform action. ' - 'Use to avoid session login via "az login". ' - 'If both an entity connection string and name are provided the connection string takes priority.') - context.argument('resource_group_name', arg_type=resource_group_name_type) - context.argument('hub_name', options_list=['--hub-name', '-n'], arg_type=hub_name_type) - context.argument('device_id', options_list=['--device-id', '-d'], help='Target Device.') - context.argument('module_id', options_list=['--module-id', '-m'], help='Target Module.') - context.argument('key_type', options_list=['--key-type', '--kt'], - arg_type=get_enum_type(KeyType), - help='Shared access policy key type for auth.') - context.argument('policy_name', options_list=['--policy-name', '--pn'], - help='Shared access policy to use for auth.') - context.argument('duration', options_list=['--duration', '--du'], type=int, - help='Valid token duration in seconds.') - context.argument('etag', options_list=['--etag', '-e'], help='Entity tag value.') - context.argument('top', type=int, options_list=['--top'], - help='Maximum number of elements to return. Use -1 for unlimited.') - context.argument('method_name', options_list=['--method-name', '--mn'], - help='Target method for invocation.') - context.argument('method_payload', options_list=['--method-payload', '--mp'], - help='Json payload to be passed to method. Must be file path or raw json.') - context.argument('timeout', options_list=['--timeout', '--to'], type=int, - help='Maximum number of seconds to wait for device method result.') - context.argument('method_connect_timeout', options_list=['--method-connect-timeout', '--mct'], type=int, - help='Maximum number of seconds to wait on device connection.') - context.argument('method_response_timeout', options_list=['--method-response-timeout', '--mrt'], type=int, - help='Maximum number of seconds to wait for device method result.') - context.argument('auth_method', options_list=['--auth-method', '--am'], - arg_type=get_enum_type(DeviceAuthType), - help='The authorization type an entity is to be created with.') - context.argument('metric_type', options_list=['--metric-type', '--mt'], arg_type=get_enum_type(MetricType), - help='Indicates which metric collection should be used to lookup a metric.') - context.argument('metric_id', options_list=['--metric-id', '-m'], - help='Target metric for evaluation.') - context.argument('yes', options_list=['--yes', '-y'], - arg_type=get_three_state_flag(), - help='Skip user prompts. Indicates acceptance of dependency installation (if required). ' - 'Used primarily for automation scenarios. Default: false') - context.argument('repair', options_list=['--repair', '-r'], - arg_type=get_three_state_flag(), - help='Reinstall uamqp dependency compatible with extension version. Default: false') - context.argument('repo_endpoint', options_list=['--endpoint', '-e'], help='IoT Plug and Play endpoint.') - context.argument('repo_id', options_list=['--repo-id', '-r'], help='IoT Plug and Play repository Id.') - context.argument('consumer_group', options_list=['--consumer-group', '--cg', '-c'], - help='Specify the consumer group to use when connecting to event hub endpoint.') - context.argument('enqueued_time', options_list=['--enqueued-time', '--et', '-e'], type=int, - help='Indicates the time that should be used as a starting point to read messages from the partitions. ' - 'Units are milliseconds since unix epoch. ' - 'If no time is indicated "now" is used.') - context.argument('content_type', options_list=['--content-type', '--ct'], - help='Specify the Content-Type of the message payload to automatically format the output to that type.') - context.argument('device_query', options_list=['--device-query', '-q'], help='Specify a custom query to filter devices.') - context.argument('edge_enabled', options_list=['--edge-enabled', '--ee'], - arg_type=get_three_state_flag(), - help='Flag indicating edge enablement.') - - with self.argument_context('iot hub') as context: - context.argument('target_json', options_list=['--json', '-j'], - help='Json to replace existing twin with. Provide file path or raw json.') - context.argument('policy_name', options_list=['--policy-name', '--pn'], - help='Shared access policy with operation permissions for target IoT Hub entity.') - context.argument('primary_thumbprint', arg_group='X.509', - options_list=['--primary-thumbprint', '--ptp'], - help='Explicit self-signed certificate thumbprint to use for primary key.') - context.argument('secondary_thumbprint', arg_group='X.509', - options_list=['--secondary-thumbprint', '--stp'], - help='Explicit self-signed certificate thumbprint to ' - 'use for secondary key.') - context.argument('valid_days', arg_group='X.509', options_list=['--valid-days', '--vd'], - type=int, - help='Generate self-signed cert and use its thumbprint. Valid ' - 'for specified number of days. Default: 365.') - context.argument('output_dir', arg_group='X.509', options_list=['--output-dir', '--od'], - help='Generate self-signed cert and use its thumbprint. ' - 'Output to specified target directory') - - with self.argument_context('iot hub job') as context: - context.argument('job_id', options_list=['--job-id'], - help='IoT Hub job Id.') - context.argument('job_status', options_list=['--job-status', '--js'], - help='The status of a scheduled job.', - arg_type=get_enum_type(JobStatusType)) - context.argument('job_type', options_list=['--job-type', '--jt'], - help='The type of scheduled job.', - arg_type=get_enum_type(JobType)) - context.argument('query_condition', options_list=['--query-condition', '-q'], - help='Condition for device query to get devices to execute the job on. ' - 'Required if job type is scheduleDeviceMethod or scheduleUpdateTwin. ' - 'Note: The service will prefix "SELECT * FROM devices WHERE " to the input') - context.argument('start_time', options_list=['--start-time', '--start'], - help='The scheduled start of the job in ISO 8601 date time format. ' - 'If no start time is provided, the job is queued for asap execution.') - context.argument('ttl', options_list=['--ttl'], type=int, - help='Max execution time in seconds, before job is terminated.') - context.argument('twin_patch', options_list=['--twin-patch', '--patch'], - help='The desired twin patch. Provide file path or raw json.') - context.argument('wait', options_list=['--wait', '-w'], - arg_type=get_three_state_flag(), - help='Block until the created job is in a completed, failed or cancelled state. ' - 'Will regularly poll on interval specified by --poll-interval.') - context.argument('poll_interval', options_list=['--poll-interval', '--interval'], type=int, - help='Interval in seconds that job status will be checked if --wait flag is passed in.') - context.argument('poll_duration', options_list=['--poll-duration', '--duration'], type=int, - help='Total duration in seconds where job status will be checked if --wait flag is passed in.') - - with self.argument_context('iot hub job create') as context: - context.argument('job_type', options_list=['--job-type', '--jt'], - help='The type of scheduled job.', - arg_type=get_enum_type(JobCreateType)) - - with self.argument_context('iot hub monitor-events') as context: - context.argument('timeout', arg_type=event_timeout_type) - context.argument('properties', arg_type=event_msg_prop_type) - - with self.argument_context('iot hub monitor-feedback') as context: - context.argument('wait_on_id', options_list=['--wait-on-msg', '-w'], - help='Feedback monitor will block until a message with specific id (uuid) is received.') - - with self.argument_context('iot hub device-identity') as context: - context.argument('status', options_list=['--status', '--sta'], - arg_type=get_enum_type(EntityStatusType), - help='Set device status upon creation.') - context.argument('status_reason', options_list=['--status-reason', '--star'], - help='Description for device status.') - - with self.argument_context('iot hub device-identity create') as context: - context.argument('force', options_list=['--force', '-f'], - help='Overwrites the non-edge device\'s parent device.') - context.argument('set_parent_id', options_list=['--set-parent', '--pd'], help='Id of edge device.') - context.argument('add_children', options_list=['--add-children', '--cl'], - help='Child device list (comma separated) includes only non-edge devices.') - - with self.argument_context('iot hub device-identity export') as context: - context.argument('blob_container_uri', - options_list=['--blob-container-uri', '--bcu'], - 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.') - context.argument('include_keys', - options_list=['--include-keys', '--ik'], - arg_type=get_three_state_flag(), - help='If set, keys are exported normally. Otherwise, keys are ' - 'set to null in export output.') - - with self.argument_context('iot hub device-identity import') as context: - context.argument('input_blob_container_uri', - options_list=['--input-blob-container-uri', '--ibcu'], - 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 ') - context.argument('output_blob_container_uri', - options_list=['--output-blob-container-uri', '--obcu'], - 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.') - - with self.argument_context('iot hub device-identity get-parent') as context: - context.argument('device_id', help='Id of non-edge device.') - - with self.argument_context('iot hub device-identity set-parent') as context: - context.argument('device_id', help='Id of non-edge 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 non-edge device\'s parent device.') - - 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) includes only non-edge devices.') - context.argument('force', options_list=['--force', '-f'], - help='Overwrites the non-edge 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) includes only non-edge devices.') - context.argument('remove_all', options_list=['--remove-all', '-a'], help='To remove all children.') - - with self.argument_context('iot hub distributed-tracing update') as context: - context.argument('sampling_mode', options_list=['--sampling-mode', '--sm'], - help='Turns sampling for distributed tracing on and off. 1 is On and, 2 is Off.', - arg_type=get_enum_type(DistributedTracingSamplingModeType)) - context.argument('sampling_rate', options_list=['--sampling-rate', '--sr'], - help='Controls the amount of messages sampled for adding trace context. This value is' - 'a percentage. Only values from 0 to 100 (inclusive) are permitted.') - - 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 query') as context: - context.argument('query_command', options_list=['--query-command', '-q'], - help='User query to be executed.') - context.argument('top', options_list=['--top'], type=int, - help='Maximum number of elements to return. By default query has no cap.') - - with self.argument_context('iot device') as context: - context.argument('data', options_list=['--data', '--da'], help='Message body.') - context.argument('properties', options_list=['--properties', '--props', '-p'], - help=info_param_properties_device()) - context.argument('msg_count', options_list=['--msg-count', '--mc'], type=int, - help='Number of device messages to send to IoT Hub.') - context.argument('msg_interval', options_list=['--msg-interval', '--mi'], type=int, - help='Delay in seconds between device-to-cloud messages.') - context.argument('receive_settle', options_list=['--receive-settle', '--rs'], - arg_type=get_enum_type(SettleType), - help='Indicates how to settle received cloud-to-device messages. ' - 'Supported with HTTP only.') - context.argument('protocol_type', options_list=['--protocol', '--proto'], - arg_type=get_enum_type(ProtocolType), - help='Indicates device-to-cloud message protocol') - context.argument('qos', arg_type=qos_type) - - with self.argument_context('iot device simulate') as context: - context.argument('properties', options_list=['--properties', '--props', '-p'], - help=info_param_properties_device(include_http=True)) - - with self.argument_context('iot device c2d-message') as context: - context.argument('ack', options_list=['--ack'], arg_type=get_enum_type(AckType), - help='Request the delivery of per-message feedback regarding the final state of that message. ' - 'The description of ack values is as follows. ' - 'Positive: If the c2d message reaches the Completed state, IoT Hub generates a feedback message. ' - 'Negative: If the c2d message reaches the Dead lettered state, IoT Hub generates a feedback message. ' - 'Full: IoT Hub generates a feedback message in either case. ' - 'By default, no ack is requested.') - context.argument('correlation_id', options_list=['--correlation-id', '--cid'], - help='The correlation Id associated with the C2D message.') - context.argument('properties', options_list=['--properties', '--props', '-p'], - help=info_param_properties_device(include_mqtt=False)) - context.argument('expiry_time_utc', options_list=['--expiry-time-utc', '--expiry'], type=int, - help='Units are milliseconds since unix epoch. ' - 'If no time is indicated the default IoT Hub C2D message TTL is used.') - context.argument('message_id', options_list=['--message-id', '--mid'], - help='The C2D message Id. If no message Id is provided a UUID will be generated.') - context.argument('user_id', options_list=['--user-id', '--uid'], - help='The C2D message, user Id property.') - context.argument('lock_timeout', options_list=['--lock-timeout', '--lt'], type=int, - help='Specifies the amount of time a message will be invisible to other receive calls.') - context.argument('content_type', options_list=['--content-type', '--ct'], - help='The content type associated with the C2D message.') - context.argument('content_encoding', options_list=['--content-encoding', '--ce'], - help='The content encoding associated with the C2D message.') - - with self.argument_context('iot device c2d-message send') as context: - context.argument('wait_on_feedback', options_list=['--wait', '-w'], - arg_type=get_three_state_flag(), - help='If set the c2d send operation will block until device feedback has been received.') - - with self.argument_context('iot device upload-file') as context: - context.argument('file_path', options_list=['--file-path', '--fp'], - help='Path to file for upload.') - context.argument('content_type', options_list=['--content-type', '--ct'], - help='MIME Type of file.') - - with self.argument_context('iot hub configuration') as context: - context.argument('config_id', options_list=['--config-id', '-c'], - help='Target device configuration name.') - context.argument('target_condition', options_list=['--target-condition', '--tc', '-t'], - help='Target condition in which a device configuration applies to.') - context.argument('priority', options_list=['--priority', '--pri'], - help='Weight of the device configuration in case of competing rules (highest wins).') - context.argument('content', options_list=['--content', '-k'], - help='Device configuration content. Provide file path or raw json.') - context.argument('metrics', options_list=['--metrics', '-m'], - help='Device configuration metric definitions. Provide file path or raw json.') - context.argument('labels', options_list=['--labels', '--lab'], - help="Map of labels to be applied to target configuration. " - "Format example: {\"key0\":\"value0\", \"key1\":\"value1\"}") - context.argument('top', options_list=['--top'], type=int, - help='Maximum number of configurations to return.') - - with self.argument_context('iot edge') as context: - context.argument('config_id', options_list=['--deployment-id', '-d'], - help='Target deployment name.') - context.argument('target_condition', options_list=['--target-condition', '--tc', '-t'], - help='Target condition in which an Edge deployment applies to.') - context.argument('priority', options_list=['--priority', '--pri'], - help='Weight of deployment in case of competing rules (highest wins).') - context.argument('content', options_list=['--content', '-k'], - help='IoT Edge deployment content. Provide file path or raw json.') - context.argument('metrics', options_list=['--metrics', '-m'], - help='IoT Edge deployment metric definitions. Provide file path or raw json.') - context.argument('labels', options_list=['--labels', '--lab'], - help="Map of labels to be applied to target deployment. " - "Use the following format: '{\"key0\":\"value0\", \"key1\":\"value1\"}'") - context.argument('top', options_list=['--top'], type=int, - help='Maximum number of deployments to return.') - context.argument('layered', options_list=['--layered'], - arg_type=get_three_state_flag(), - help='Layered deployments allow you to define desired properties in $edgeAgent, $edgeHub and user ' - 'modules that will layer on top of a base deployment. For example the routes specified in a layered ' - 'deployment will merge with routes of the base deployment. Routes with the same name will be ' - 'overwritten based on deployment priority.' - ) - - with self.argument_context('iot dps') as context: - context.argument('dps_name', help='Name of the Azure IoT Hub device provisioning service') - context.argument('initial_twin_properties', - options_list=['--initial-twin-properties', '--props'], - help='Initial twin properties') - context.argument('initial_twin_tags', options_list=['--initial-twin-tags', '--tags'], - help='Initial twin tags') - context.argument('iot_hub_host_name', options_list=['--iot-hub-host-name', '--hn'], - help='Host name of target IoT Hub') - context.argument('provisioning_status', options_list=['--provisioning-status', '--ps'], - arg_type=get_enum_type(EntityStatusType), - help='Enable or disable enrollment entry') - context.argument('certificate_path', - options_list=['--certificate-path', '--cp'], - help='The path to the file containing the primary certificate.') - context.argument('secondary_certificate_path', - options_list=['--secondary-certificate-path', '--scp'], - help='The path to the file containing the secondary certificate') - context.argument('remove_certificate', - options_list=['--remove-certificate', '--rc'], - help='Remove current primary certificate', - arg_type=get_three_state_flag()) - context.argument('remove_secondary_certificate', - options_list=['--remove-secondary-certificate', '--rsc'], - help='Remove current secondary certificate', - arg_type=get_three_state_flag()) - context.argument('reprovision_policy', options_list=['--reprovision-policy', '--rp'], - arg_type=get_enum_type(ReprovisionType), - help='Device data to be handled on re-provision to different Iot Hub.') - context.argument('allocation_policy', options_list=['--allocation-policy', '--ap'], - arg_type=get_enum_type(AllocationType), - help='Type of allocation for device assigned to the Hub.') - context.argument('iot_hubs', options_list=['--iot-hubs', '--ih'], - help='Host name of target IoT Hub. Use space-separated list for multiple IoT Hubs.') - - with self.argument_context('iot dps enrollment') as context: - context.argument('enrollment_id', help='ID of device enrollment record') - context.argument('device_id', help='IoT Hub Device ID') - context.argument('primary_key', options_list=['--primary-key', '--pk'], - help='The primary symmetric shared access key stored in base64 format. ') - context.argument('secondary_key', options_list=['--secondary-key', '--sk'], - help='The secondary symmetric shared access key stored in base64 format. ') - - with self.argument_context('iot dps enrollment create') as context: - context.argument('attestation_type', options_list=['--attestation-type', '--at'], - arg_type=get_enum_type(AttestationType), help='Attestation Mechanism') - context.argument('certificate_path', - options_list=['--certificate-path', '--cp'], - help='The path to the file containing the primary certificate. ' - 'When choosing x509 as attestation type, ' - 'one of the certificate path is required.') - context.argument('secondary_certificate_path', - options_list=['--secondary-certificate-path', '--scp'], - help='The path to the file containing the secondary certificate. ' - 'When choosing x509 as attestation type, ' - 'one of the certificate path is required.') - context.argument('endorsement_key', options_list=['--endorsement-key', '--ek'], - help='TPM endorsement key for a TPM device. ' - 'When choosing tpm as attestation type, endorsement key is required.') - - with self.argument_context('iot dps enrollment update') as context: - context.argument('endorsement_key', options_list=['--endorsement-key', '--ek'], - help='TPM endorsement key for a TPM device.') - - with self.argument_context('iot dps enrollment-group') as context: - context.argument('enrollment_id', help='ID of enrollment group') - context.argument('primary_key', options_list=['--primary-key', '--pk'], - help='The primary symmetric shared access key stored in base64 format. ') - context.argument('secondary_key', options_list=['--secondary-key', '--sk'], - help='The secondary symmetric shared access key stored in base64 format. ') - context.argument('certificate_path', - options_list=['--certificate-path', '--cp'], - help='The path to the file containing the primary certificate. ' - 'If attestation with an intermediate certificate is desired then a certificate path must be provided.') - context.argument('secondary_certificate_path', - options_list=['--secondary-certificate-path', '--scp'], - help='The path to the file containing the secondary certificate. ' - 'If attestation with an intermediate certificate is desired then a certificate path must be provided.') - context.argument('root_ca_name', - options_list=['--root-ca-name', '--ca-name', '--cn'], - help='The name of the primary root CA certificate. ' - 'If attestation with a root CA certificate is desired then a root ca name must be provided.') - context.argument('secondary_root_ca_name', - options_list=['--secondary-root-ca-name', '--secondary-ca-name', '--scn'], - help='The name of the secondary root CA certificate. ' - 'If attestation with a root CA certificate is desired then a root ca name must be provided.') - - with self.argument_context('iot dps registration') as context: - context.argument('registration_id', help='ID of device registration') - - with self.argument_context('iot dps registration list') as context: - context.argument('enrollment_id', help='ID of enrollment group') + with self.argument_context("iot") as context: + context.argument( + "login", + options_list=["--login", "-l"], + validator=mode2_iot_login_handler, + help="This command supports an entity connection string with rights to perform action. " + 'Use to avoid session login via "az login". ' + "If both an entity connection string and name are provided the connection string takes priority.", + ) + context.argument("resource_group_name", arg_type=resource_group_name_type) + context.argument( + "hub_name", options_list=["--hub-name", "-n"], arg_type=hub_name_type + ) + context.argument( + "device_id", options_list=["--device-id", "-d"], help="Target Device." + ) + context.argument( + "module_id", options_list=["--module-id", "-m"], help="Target Module." + ) + context.argument( + "key_type", + options_list=["--key-type", "--kt"], + arg_type=get_enum_type(KeyType), + help="Shared access policy key type for auth.", + ) + context.argument( + "policy_name", + options_list=["--policy-name", "--pn"], + help="Shared access policy to use for auth.", + ) + context.argument( + "duration", + options_list=["--duration", "--du"], + type=int, + help="Valid token duration in seconds.", + ) + context.argument( + "etag", options_list=["--etag", "-e"], help="Entity tag value." + ) + context.argument( + "top", + type=int, + options_list=["--top"], + help="Maximum number of elements to return. Use -1 for unlimited.", + ) + context.argument( + "method_name", + options_list=["--method-name", "--mn"], + help="Target method for invocation.", + ) + context.argument( + "method_payload", + options_list=["--method-payload", "--mp"], + help="Json payload to be passed to method. Must be file path or raw json.", + ) + context.argument( + "timeout", + options_list=["--timeout", "--to"], + type=int, + help="Maximum number of seconds to wait for device method result.", + ) + context.argument( + "method_connect_timeout", + options_list=["--method-connect-timeout", "--mct"], + type=int, + help="Maximum number of seconds to wait on device connection.", + ) + context.argument( + "method_response_timeout", + options_list=["--method-response-timeout", "--mrt"], + type=int, + help="Maximum number of seconds to wait for device method result.", + ) + context.argument( + "auth_method", + options_list=["--auth-method", "--am"], + arg_type=get_enum_type(DeviceAuthType), + help="The authorization type an entity is to be created with.", + ) + context.argument( + "metric_type", + options_list=["--metric-type", "--mt"], + arg_type=get_enum_type(MetricType), + help="Indicates which metric collection should be used to lookup a metric.", + ) + context.argument( + "metric_id", + options_list=["--metric-id", "-m"], + help="Target metric for evaluation.", + ) + context.argument( + "yes", + options_list=["--yes", "-y"], + arg_type=get_three_state_flag(), + help="Skip user prompts. Indicates acceptance of dependency installation (if required). " + "Used primarily for automation scenarios. Default: false", + ) + context.argument( + "repair", + options_list=["--repair", "-r"], + arg_type=get_three_state_flag(), + help="Reinstall uamqp dependency compatible with extension version. Default: false", + ) + context.argument( + "repo_endpoint", + options_list=["--endpoint", "-e"], + help="IoT Plug and Play endpoint.", + ) + context.argument( + "repo_id", + options_list=["--repo-id", "-r"], + help="IoT Plug and Play repository Id.", + ) + context.argument( + "consumer_group", + options_list=["--consumer-group", "--cg", "-c"], + help="Specify the consumer group to use when connecting to event hub endpoint.", + ) + context.argument( + "enqueued_time", + options_list=["--enqueued-time", "--et", "-e"], + type=int, + help="Indicates the time that should be used as a starting point to read messages from the partitions. " + "Units are milliseconds since unix epoch. " + 'If no time is indicated "now" is used.', + ) + context.argument( + "content_type", + options_list=["--content-type", "--ct"], + help="Specify the Content-Type of the message payload to automatically format the output to that type.", + ) + context.argument( + "device_query", + options_list=["--device-query", "-q"], + help="Specify a custom query to filter devices.", + ) + context.argument( + "edge_enabled", + options_list=["--edge-enabled", "--ee"], + arg_type=get_three_state_flag(), + help="Flag indicating edge enablement.", + ) + + with self.argument_context("iot hub") as context: + context.argument( + "target_json", + options_list=["--json", "-j"], + help="Json to replace existing twin with. Provide file path or raw json.", + ) + context.argument( + "policy_name", + options_list=["--policy-name", "--pn"], + help="Shared access policy with operation permissions for target IoT Hub entity.", + ) + context.argument( + "primary_thumbprint", + arg_group="X.509", + options_list=["--primary-thumbprint", "--ptp"], + help="Explicit self-signed certificate thumbprint to use for primary key.", + ) + context.argument( + "secondary_thumbprint", + arg_group="X.509", + options_list=["--secondary-thumbprint", "--stp"], + help="Explicit self-signed certificate thumbprint to " + "use for secondary key.", + ) + context.argument( + "valid_days", + arg_group="X.509", + options_list=["--valid-days", "--vd"], + type=int, + help="Generate self-signed cert and use its thumbprint. Valid " + "for specified number of days. Default: 365.", + ) + context.argument( + "output_dir", + arg_group="X.509", + options_list=["--output-dir", "--od"], + help="Generate self-signed cert and use its thumbprint. " + "Output to specified target directory", + ) + + with self.argument_context("iot hub job") as context: + context.argument("job_id", options_list=["--job-id"], help="IoT Hub job Id.") + context.argument( + "job_status", + options_list=["--job-status", "--js"], + help="The status of a scheduled job.", + arg_type=get_enum_type(JobStatusType), + ) + context.argument( + "job_type", + options_list=["--job-type", "--jt"], + help="The type of scheduled job.", + arg_type=get_enum_type(JobType), + ) + context.argument( + "query_condition", + options_list=["--query-condition", "-q"], + help="Condition for device query to get devices to execute the job on. " + "Required if job type is scheduleDeviceMethod or scheduleUpdateTwin. " + 'Note: The service will prefix "SELECT * FROM devices WHERE " to the input', + ) + context.argument( + "start_time", + options_list=["--start-time", "--start"], + help="The scheduled start of the job in ISO 8601 date time format. " + "If no start time is provided, the job is queued for asap execution.", + ) + context.argument( + "ttl", + options_list=["--ttl"], + type=int, + help="Max execution time in seconds, before job is terminated.", + ) + context.argument( + "twin_patch", + options_list=["--twin-patch", "--patch"], + help="The desired twin patch. Provide file path or raw json.", + ) + context.argument( + "wait", + options_list=["--wait", "-w"], + arg_type=get_three_state_flag(), + help="Block until the created job is in a completed, failed or cancelled state. " + "Will regularly poll on interval specified by --poll-interval.", + ) + context.argument( + "poll_interval", + options_list=["--poll-interval", "--interval"], + type=int, + help="Interval in seconds that job status will be checked if --wait flag is passed in.", + ) + context.argument( + "poll_duration", + options_list=["--poll-duration", "--duration"], + type=int, + help="Total duration in seconds where job status will be checked if --wait flag is passed in.", + ) + + with self.argument_context("iot hub job create") as context: + context.argument( + "job_type", + options_list=["--job-type", "--jt"], + help="The type of scheduled job.", + arg_type=get_enum_type(JobCreateType), + ) + + with self.argument_context("iot hub monitor-events") as context: + context.argument("timeout", arg_type=event_timeout_type) + context.argument("properties", arg_type=event_msg_prop_type) + + with self.argument_context("iot hub monitor-feedback") as context: + context.argument( + "wait_on_id", + options_list=["--wait-on-msg", "-w"], + help="Feedback monitor will block until a message with specific id (uuid) is received.", + ) + + with self.argument_context("iot hub device-identity") as context: + context.argument( + "status", + options_list=["--status", "--sta"], + arg_type=get_enum_type(EntityStatusType), + help="Set device status upon creation.", + ) + context.argument( + "status_reason", + options_list=["--status-reason", "--star"], + help="Description for device status.", + ) + + with self.argument_context("iot hub device-identity create") as context: + context.argument( + "force", + options_list=["--force", "-f"], + help="Overwrites the non-edge device's parent device.", + ) + context.argument( + "set_parent_id", + options_list=["--set-parent", "--pd"], + help="Id of edge device.", + ) + context.argument( + "add_children", + options_list=["--add-children", "--cl"], + help="Child device list (comma separated) includes only non-edge devices.", + ) + + with self.argument_context("iot hub device-identity export") as context: + context.argument( + "blob_container_uri", + options_list=["--blob-container-uri", "--bcu"], + 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.", + ) + context.argument( + "include_keys", + options_list=["--include-keys", "--ik"], + arg_type=get_three_state_flag(), + help="If set, keys are exported normally. Otherwise, keys are " + "set to null in export output.", + ) + + with self.argument_context("iot hub device-identity import") as context: + context.argument( + "input_blob_container_uri", + options_list=["--input-blob-container-uri", "--ibcu"], + 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 ", + ) + context.argument( + "output_blob_container_uri", + options_list=["--output-blob-container-uri", "--obcu"], + 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.", + ) + + with self.argument_context("iot hub device-identity get-parent") as context: + context.argument("device_id", help="Id of non-edge device.") + + with self.argument_context("iot hub device-identity set-parent") as context: + context.argument("device_id", help="Id of non-edge 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 non-edge device's parent device.", + ) + + 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) includes only non-edge devices.", + ) + context.argument( + "force", + options_list=["--force", "-f"], + help="Overwrites the non-edge 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) includes only non-edge devices.", + ) + context.argument( + "remove_all", + options_list=["--remove-all", "-a"], + help="To remove all children.", + ) + + with self.argument_context("iot hub distributed-tracing update") as context: + context.argument( + "sampling_mode", + options_list=["--sampling-mode", "--sm"], + help="Turns sampling for distributed tracing on and off. 1 is On and, 2 is Off.", + arg_type=get_enum_type(DistributedTracingSamplingModeType), + ) + context.argument( + "sampling_rate", + options_list=["--sampling-rate", "--sr"], + help="Controls the amount of messages sampled for adding trace context. This value is" + "a percentage. Only values from 0 to 100 (inclusive) are permitted.", + ) + + 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 query") as context: + context.argument( + "query_command", + options_list=["--query-command", "-q"], + help="User query to be executed.", + ) + context.argument( + "top", + options_list=["--top"], + type=int, + help="Maximum number of elements to return. By default query has no cap.", + ) + + with self.argument_context("iot device") as context: + context.argument("data", options_list=["--data", "--da"], help="Message body.") + context.argument( + "properties", + options_list=["--properties", "--props", "-p"], + help=info_param_properties_device(), + ) + context.argument( + "msg_count", + options_list=["--msg-count", "--mc"], + type=int, + help="Number of device messages to send to IoT Hub.", + ) + context.argument( + "msg_interval", + options_list=["--msg-interval", "--mi"], + type=int, + help="Delay in seconds between device-to-cloud messages.", + ) + context.argument( + "receive_settle", + options_list=["--receive-settle", "--rs"], + arg_type=get_enum_type(SettleType), + help="Indicates how to settle received cloud-to-device messages. " + "Supported with HTTP only.", + ) + context.argument( + "protocol_type", + options_list=["--protocol", "--proto"], + arg_type=get_enum_type(ProtocolType), + help="Indicates device-to-cloud message protocol", + ) + context.argument("qos", arg_type=qos_type) + + with self.argument_context("iot device simulate") as context: + context.argument( + "properties", + options_list=["--properties", "--props", "-p"], + help=info_param_properties_device(include_http=True), + ) + + with self.argument_context("iot device c2d-message") as context: + context.argument( + "ack", + options_list=["--ack"], + arg_type=get_enum_type(AckType), + help="Request the delivery of per-message feedback regarding the final state of that message. " + "The description of ack values is as follows. " + "Positive: If the c2d message reaches the Completed state, IoT Hub generates a feedback message. " + "Negative: If the c2d message reaches the Dead lettered state, IoT Hub generates a feedback message. " + "Full: IoT Hub generates a feedback message in either case. " + "By default, no ack is requested.", + ) + context.argument( + "correlation_id", + options_list=["--correlation-id", "--cid"], + help="The correlation Id associated with the C2D message.", + ) + context.argument( + "properties", + options_list=["--properties", "--props", "-p"], + help=info_param_properties_device(include_mqtt=False), + ) + context.argument( + "expiry_time_utc", + options_list=["--expiry-time-utc", "--expiry"], + type=int, + help="Units are milliseconds since unix epoch. " + "If no time is indicated the default IoT Hub C2D message TTL is used.", + ) + context.argument( + "message_id", + options_list=["--message-id", "--mid"], + help="The C2D message Id. If no message Id is provided a UUID will be generated.", + ) + context.argument( + "user_id", + options_list=["--user-id", "--uid"], + help="The C2D message, user Id property.", + ) + context.argument( + "lock_timeout", + options_list=["--lock-timeout", "--lt"], + type=int, + help="Specifies the amount of time a message will be invisible to other receive calls.", + ) + context.argument( + "content_type", + options_list=["--content-type", "--ct"], + help="The content type associated with the C2D message.", + ) + context.argument( + "content_encoding", + options_list=["--content-encoding", "--ce"], + help="The content encoding associated with the C2D message.", + ) + + with self.argument_context("iot device c2d-message send") as context: + context.argument( + "wait_on_feedback", + options_list=["--wait", "-w"], + arg_type=get_three_state_flag(), + help="If set the c2d send operation will block until device feedback has been received.", + ) + + with self.argument_context("iot device upload-file") as context: + context.argument( + "file_path", + options_list=["--file-path", "--fp"], + help="Path to file for upload.", + ) + context.argument( + "content_type", + options_list=["--content-type", "--ct"], + help="MIME Type of file.", + ) + + with self.argument_context("iot hub configuration") as context: + context.argument( + "config_id", + options_list=["--config-id", "-c"], + help="Target device configuration name.", + ) + context.argument( + "target_condition", + options_list=["--target-condition", "--tc", "-t"], + help="Target condition in which a device configuration applies to.", + ) + context.argument( + "priority", + options_list=["--priority", "--pri"], + help="Weight of the device configuration in case of competing rules (highest wins).", + ) + context.argument( + "content", + options_list=["--content", "-k"], + help="Device configuration content. Provide file path or raw json.", + ) + context.argument( + "metrics", + options_list=["--metrics", "-m"], + help="Device configuration metric definitions. Provide file path or raw json.", + ) + context.argument( + "labels", + options_list=["--labels", "--lab"], + help="Map of labels to be applied to target configuration. " + 'Format example: {"key0":"value0", "key1":"value1"}', + ) + context.argument( + "top", + options_list=["--top"], + type=int, + help="Maximum number of configurations to return.", + ) + + with self.argument_context("iot edge") as context: + context.argument( + "config_id", + options_list=["--deployment-id", "-d"], + help="Target deployment name.", + ) + context.argument( + "target_condition", + options_list=["--target-condition", "--tc", "-t"], + help="Target condition in which an Edge deployment applies to.", + ) + context.argument( + "priority", + options_list=["--priority", "--pri"], + help="Weight of deployment in case of competing rules (highest wins).", + ) + context.argument( + "content", + options_list=["--content", "-k"], + help="IoT Edge deployment content. Provide file path or raw json.", + ) + context.argument( + "metrics", + options_list=["--metrics", "-m"], + help="IoT Edge deployment metric definitions. Provide file path or raw json.", + ) + context.argument( + "labels", + options_list=["--labels", "--lab"], + help="Map of labels to be applied to target deployment. " + 'Use the following format: \'{"key0":"value0", "key1":"value1"}\'', + ) + context.argument( + "top", + options_list=["--top"], + type=int, + help="Maximum number of deployments to return.", + ) + context.argument( + "layered", + options_list=["--layered"], + arg_type=get_three_state_flag(), + help="Layered deployments allow you to define desired properties in $edgeAgent, $edgeHub and user " + "modules that will layer on top of a base deployment. For example the routes specified in a layered " + "deployment will merge with routes of the base deployment. Routes with the same name will be " + "overwritten based on deployment priority.", + ) + + with self.argument_context("iot dps") as context: + context.argument( + "dps_name", help="Name of the Azure IoT Hub device provisioning service" + ) + context.argument( + "initial_twin_properties", + options_list=["--initial-twin-properties", "--props"], + help="Initial twin properties", + ) + context.argument( + "initial_twin_tags", + options_list=["--initial-twin-tags", "--tags"], + help="Initial twin tags", + ) + context.argument( + "iot_hub_host_name", + options_list=["--iot-hub-host-name", "--hn"], + help="Host name of target IoT Hub", + ) + context.argument( + "provisioning_status", + options_list=["--provisioning-status", "--ps"], + arg_type=get_enum_type(EntityStatusType), + help="Enable or disable enrollment entry", + ) + context.argument( + "certificate_path", + options_list=["--certificate-path", "--cp"], + help="The path to the file containing the primary certificate.", + ) + context.argument( + "secondary_certificate_path", + options_list=["--secondary-certificate-path", "--scp"], + help="The path to the file containing the secondary certificate", + ) + context.argument( + "remove_certificate", + options_list=["--remove-certificate", "--rc"], + help="Remove current primary certificate", + arg_type=get_three_state_flag(), + ) + context.argument( + "remove_secondary_certificate", + options_list=["--remove-secondary-certificate", "--rsc"], + help="Remove current secondary certificate", + arg_type=get_three_state_flag(), + ) + context.argument( + "reprovision_policy", + options_list=["--reprovision-policy", "--rp"], + arg_type=get_enum_type(ReprovisionType), + help="Device data to be handled on re-provision to different Iot Hub.", + ) + context.argument( + "allocation_policy", + options_list=["--allocation-policy", "--ap"], + arg_type=get_enum_type(AllocationType), + help="Type of allocation for device assigned to the Hub.", + ) + context.argument( + "iot_hubs", + options_list=["--iot-hubs", "--ih"], + help="Host name of target IoT Hub. Use space-separated list for multiple IoT Hubs.", + ) + + with self.argument_context("iot dps enrollment") as context: + context.argument("enrollment_id", help="ID of device enrollment record") + context.argument("device_id", help="IoT Hub Device ID") + context.argument( + "primary_key", + options_list=["--primary-key", "--pk"], + help="The primary symmetric shared access key stored in base64 format. ", + ) + context.argument( + "secondary_key", + options_list=["--secondary-key", "--sk"], + help="The secondary symmetric shared access key stored in base64 format. ", + ) + + with self.argument_context("iot dps enrollment create") as context: + context.argument( + "attestation_type", + options_list=["--attestation-type", "--at"], + arg_type=get_enum_type(AttestationType), + help="Attestation Mechanism", + ) + context.argument( + "certificate_path", + options_list=["--certificate-path", "--cp"], + help="The path to the file containing the primary certificate. " + "When choosing x509 as attestation type, " + "one of the certificate path is required.", + ) + context.argument( + "secondary_certificate_path", + options_list=["--secondary-certificate-path", "--scp"], + help="The path to the file containing the secondary certificate. " + "When choosing x509 as attestation type, " + "one of the certificate path is required.", + ) + context.argument( + "endorsement_key", + options_list=["--endorsement-key", "--ek"], + help="TPM endorsement key for a TPM device. " + "When choosing tpm as attestation type, endorsement key is required.", + ) + + with self.argument_context("iot dps enrollment update") as context: + context.argument( + "endorsement_key", + options_list=["--endorsement-key", "--ek"], + help="TPM endorsement key for a TPM device.", + ) + + with self.argument_context("iot dps enrollment-group") as context: + context.argument("enrollment_id", help="ID of enrollment group") + context.argument( + "primary_key", + options_list=["--primary-key", "--pk"], + help="The primary symmetric shared access key stored in base64 format. ", + ) + context.argument( + "secondary_key", + options_list=["--secondary-key", "--sk"], + help="The secondary symmetric shared access key stored in base64 format. ", + ) + context.argument( + "certificate_path", + options_list=["--certificate-path", "--cp"], + help="The path to the file containing the primary certificate. " + "If attestation with an intermediate certificate is desired then a certificate path must be provided.", + ) + context.argument( + "secondary_certificate_path", + options_list=["--secondary-certificate-path", "--scp"], + help="The path to the file containing the secondary certificate. " + "If attestation with an intermediate certificate is desired then a certificate path must be provided.", + ) + context.argument( + "root_ca_name", + options_list=["--root-ca-name", "--ca-name", "--cn"], + help="The name of the primary root CA certificate. " + "If attestation with a root CA certificate is desired then a root ca name must be provided.", + ) + context.argument( + "secondary_root_ca_name", + options_list=["--secondary-root-ca-name", "--secondary-ca-name", "--scn"], + help="The name of the secondary root CA certificate. " + "If attestation with a root CA certificate is desired then a root ca name must be provided.", + ) + + with self.argument_context("iot dps registration") as context: + context.argument("registration_id", help="ID of device registration") + + with self.argument_context("iot dps registration list") as context: + context.argument("enrollment_id", help="ID of enrollment group") # TODO: Remove 'iotcentral', non-conventional and no reuse of arguments. 'iot central' is the go forward. - with self.argument_context('iotcentral app monitor-events') as context: - context.argument('device_id', options_list=['--device-id', '-d'], help='Target Device.') - context.argument('app_id', options_list=['--app-id'], help='Target App.') - context.argument('timeout', options_list=['--timeout', '--to', '-t'], type=int, - help='Maximum seconds to maintain connection without receiving message. Use 0 for infinity. ') - context.argument('consumer_group', options_list=['--consumer-group', '--cg', '-c'], - help='Specify the consumer group to use when connecting to event hub endpoint.') - context.argument('enqueued_time', options_list=['--enqueued-time', '--et', '-e'], type=int, - help='Indicates the time that should be used as a starting point to read messages from the partitions. ' - 'Units are milliseconds since unix epoch. ' - 'If no time is indicated "now" is used.') - context.argument('properties', arg_type=event_msg_prop_type) - context.argument('content_type', options_list=['--content-type', '--ct'], - help='Specify the Content-Type of the message payload to automatically format the output to that type.') - context.argument('repair', options_list=['--repair', '-r'], - arg_type=get_three_state_flag(), - help='Reinstall uamqp dependency compatible with extension version. Default: false') - context.argument('central_api_uri', options_list=['--central-api-uri'], - help='IoT Central API override. For use with environments other than production.') - context.argument('yes', options_list=['--yes', '-y'], - arg_type=get_three_state_flag(), - help='Skip user prompts. Indicates acceptance of dependency installation (if required). ' - 'Used primarily for automation scenarios. Default: false') - - with self.argument_context('iotcentral device-twin show') as context: - context.argument('device_id', options_list=['--device-id', '-d'], help='Target Device.') - context.argument('app_id', options_list=['--app-id'], help='Target App.') - context.argument('central_api_uri', options_list=['--central-api-uri'], - help='IoT Central API override. For use with environments other than production.') - - with self.argument_context('iot central') as context: - context.argument('app_id', options_list=['--app-id'], help='Target App.') - context.argument('central_api_uri', options_list=['--central-api-uri'], - help='IoT Central API override. For use with environments other than production.') - - with self.argument_context('iot central app monitor-events') as context: - context.argument('timeout', arg_type=event_timeout_type) - context.argument('properties', arg_type=event_msg_prop_type) - - with self.argument_context('iot dt') as context: - context.argument('repo_login', options_list=['--repo-login', '--rl'], - help='This command supports an entity connection string with rights to perform action. ' - 'Use to avoid PnP endpoint and repository name if repository is private. ' - 'If both an entity connection string and name are provided the connection string takes priority.') - context.argument('interface', options_list=['--interface', '-i'], - help='Target interface name. This should be the name of the interface not the urn-id.') - context.argument('command_name', options_list=['--command-name', '--cn'], - help='IoT Plug and Play interface command name.') - context.argument('command_payload', options_list=['--command-payload', '--cp', '--cv'], - help='IoT Plug and Play interface command payload. ' - 'Content can be directly input or extracted from a file path.') - context.argument('interface_payload', options_list=['--interface-payload', '--ip', '--iv'], - help='IoT Plug and Play interface payload. ' - 'Content can be directly input or extracted from a file path.') - context.argument('source_model', options_list=['--source', '-s'], - help='Choose your option to get model definition from specified source. ', - arg_type=get_enum_type(ModelSourceType)) - context.argument('schema', options_list=['--schema'], - help='Show interface with entity schema.') - - with self.argument_context('iot dt monitor-events') as context: - context.argument('timeout', arg_type=event_timeout_type) - context.argument('properties', arg_type=event_msg_prop_type) - - with self.argument_context('iot pnp') as context: - context.argument('model', options_list=['--model', '-m'], - help='Target capability-model urn-id. Example: urn:example:capabilityModels:Mxchip:1') - context.argument('interface', options_list=['--interface', '-i'], - help='Target interface urn-id. Example: urn:example:interfaces:MXChip:1') - - with self.argument_context('iot pnp interface') as context: - context.argument('interface_definition', options_list=['--definition', '--def'], - help='IoT Plug and Play interface definition written in PPDL (JSON-LD). ' - 'Can be directly input or a file path where the content is extracted.') - - with self.argument_context('iot pnp interface list') as context: - context.argument('search_string', options_list=['--search', '--ss'], - help='Searches IoT Plug and Play interfaces for given string in the' - ' \"Description, DisplayName, comment and Id\".') - context.argument('top', type=int, options_list=['--top'], - help='Maximum number of interface to return.') - - with self.argument_context('iot pnp capability-model') as context: - context.argument('model_definition', options_list=['--definition', '--def'], - help='IoT Plug and Play capability-model definition written in PPDL (JSON-LD). ' - 'Can be directly input or a file path where the content is extracted.') - - with self.argument_context('iot pnp capability-model show') as context: - context.argument('expand', options_list=['--expand'], - help='Indicates whether to expand the device capability model\'s' - ' interface definitions or not.') - - with self.argument_context('iot pnp capability-model list') as context: - context.argument('search_string', options_list=['--search', '--ss'], - help='Searches IoT Plug and Play models for given string in the' - ' \"Description, DisplayName, comment and Id\".') - context.argument('top', type=int, options_list=['--top'], - help='Maximum number of capability-model to return.') + with self.argument_context("iotcentral app monitor-events") as context: + context.argument( + "device_id", options_list=["--device-id", "-d"], help="Target Device." + ) + context.argument("app_id", options_list=["--app-id"], help="Target App.") + context.argument( + "timeout", + options_list=["--timeout", "--to", "-t"], + type=int, + help="Maximum seconds to maintain connection without receiving message. Use 0 for infinity. ", + ) + context.argument( + "consumer_group", + options_list=["--consumer-group", "--cg", "-c"], + help="Specify the consumer group to use when connecting to event hub endpoint.", + ) + context.argument( + "enqueued_time", + options_list=["--enqueued-time", "--et", "-e"], + type=int, + help="Indicates the time that should be used as a starting point to read messages from the partitions. " + "Units are milliseconds since unix epoch. " + 'If no time is indicated "now" is used.', + ) + context.argument("properties", arg_type=event_msg_prop_type) + context.argument( + "content_type", + options_list=["--content-type", "--ct"], + help="Specify the Content-Type of the message payload to automatically format the output to that type.", + ) + context.argument( + "repair", + options_list=["--repair", "-r"], + arg_type=get_three_state_flag(), + help="Reinstall uamqp dependency compatible with extension version. Default: false", + ) + context.argument( + "central_api_uri", + options_list=["--central-api-uri"], + help="IoT Central API override. For use with environments other than production.", + ) + context.argument( + "yes", + options_list=["--yes", "-y"], + arg_type=get_three_state_flag(), + help="Skip user prompts. Indicates acceptance of dependency installation (if required). " + "Used primarily for automation scenarios. Default: false", + ) + + with self.argument_context("iotcentral device-twin show") as context: + context.argument( + "device_id", options_list=["--device-id", "-d"], help="Target Device." + ) + context.argument("app_id", options_list=["--app-id"], help="Target App.") + context.argument( + "central_api_uri", + options_list=["--central-api-uri"], + help="IoT Central API override. For use with environments other than production.", + ) + + with self.argument_context("iot central") as context: + context.argument("app_id", options_list=["--app-id"], help="Target App.") + context.argument( + "central_api_uri", + options_list=["--central-api-uri"], + help="IoT Central API override. For use with environments other than production.", + ) + + with self.argument_context("iot central app") as context: + context.argument("properties", arg_type=event_msg_prop_type) + + with self.argument_context("iot central app validate-messages") as context: + context.argument( + "simulate_errors", + arg_type=get_three_state_flag(), + help="Randomly converts messages received into errors. Used primarily for testing purposes. Default: false", + ) + + with self.argument_context("iot dt") as context: + context.argument( + "repo_login", + options_list=["--repo-login", "--rl"], + help="This command supports an entity connection string with rights to perform action. " + "Use to avoid PnP endpoint and repository name if repository is private. " + "If both an entity connection string and name are provided the connection string takes priority.", + ) + context.argument( + "interface", + options_list=["--interface", "-i"], + help="Target interface name. This should be the name of the interface not the urn-id.", + ) + context.argument( + "command_name", + options_list=["--command-name", "--cn"], + help="IoT Plug and Play interface command name.", + ) + context.argument( + "command_payload", + options_list=["--command-payload", "--cp", "--cv"], + help="IoT Plug and Play interface command payload. " + "Content can be directly input or extracted from a file path.", + ) + context.argument( + "interface_payload", + options_list=["--interface-payload", "--ip", "--iv"], + help="IoT Plug and Play interface payload. " + "Content can be directly input or extracted from a file path.", + ) + context.argument( + "source_model", + options_list=["--source", "-s"], + help="Choose your option to get model definition from specified source. ", + arg_type=get_enum_type(ModelSourceType), + ) + context.argument( + "schema", + options_list=["--schema"], + help="Show interface with entity schema.", + ) + + with self.argument_context("iot dt monitor-events") as context: + context.argument("timeout", arg_type=event_timeout_type) + context.argument("properties", arg_type=event_msg_prop_type) + + with self.argument_context("iot pnp") as context: + context.argument( + "model", + options_list=["--model", "-m"], + help="Target capability-model urn-id. Example: urn:example:capabilityModels:Mxchip:1", + ) + context.argument( + "interface", + options_list=["--interface", "-i"], + help="Target interface urn-id. Example: urn:example:interfaces:MXChip:1", + ) + + with self.argument_context("iot pnp interface") as context: + context.argument( + "interface_definition", + options_list=["--definition", "--def"], + help="IoT Plug and Play interface definition written in PPDL (JSON-LD). " + "Can be directly input or a file path where the content is extracted.", + ) + + with self.argument_context("iot pnp interface list") as context: + context.argument( + "search_string", + options_list=["--search", "--ss"], + help="Searches IoT Plug and Play interfaces for given string in the" + ' "Description, DisplayName, comment and Id".', + ) + context.argument( + "top", + type=int, + options_list=["--top"], + help="Maximum number of interface to return.", + ) + + with self.argument_context("iot pnp capability-model") as context: + context.argument( + "model_definition", + options_list=["--definition", "--def"], + help="IoT Plug and Play capability-model definition written in PPDL (JSON-LD). " + "Can be directly input or a file path where the content is extracted.", + ) + + with self.argument_context("iot pnp capability-model show") as context: + context.argument( + "expand", + options_list=["--expand"], + help="Indicates whether to expand the device capability model's" + " interface definitions or not.", + ) + + with self.argument_context("iot pnp capability-model list") as context: + context.argument( + "search_string", + options_list=["--search", "--ss"], + help="Searches IoT Plug and Play models for given string in the" + ' "Description, DisplayName, comment and Id".', + ) + context.argument( + "top", + type=int, + options_list=["--top"], + help="Maximum number of capability-model to return.", + ) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 0898e2d1b..7fdfc0d88 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -8,150 +8,213 @@ Load CLI commands """ -from azext_iot import iothub_ops, iotdps_ops, iotdigitaltwin_ops, iotpnp_ops, iotcentral_ops +from azext_iot import ( + iothub_ops, + iotdps_ops, + iotdigitaltwin_ops, + iotpnp_ops, + iotcentral_ops, +) def load_command_table(self, _): """ Load CLI commands """ - with self.command_group('iot hub', command_type=iothub_ops) as cmd_group: - cmd_group.command('query', 'iot_query') - cmd_group.command('invoke-device-method', 'iot_device_method') - cmd_group.command('invoke-module-method', 'iot_device_module_method') - cmd_group.command('generate-sas-token', 'iot_get_sas_token') - cmd_group.command('monitor-events', 'iot_hub_monitor_events') - cmd_group.command('monitor-feedback', 'iot_hub_monitor_feedback') - - with self.command_group('iot hub device-identity', command_type=iothub_ops) as cmd_group: - cmd_group.command('create', 'iot_device_create') - cmd_group.command('show', 'iot_device_show') - cmd_group.command('list', 'iot_device_list') - cmd_group.command('delete', 'iot_device_delete') - cmd_group.generic_update_command('update', getter_name='iot_device_show', - setter_name='iot_device_update') - - cmd_group.command('show-connection-string', 'iot_get_device_connection_string') - cmd_group.command('import', 'iot_device_import') - cmd_group.command('export', 'iot_device_export') - cmd_group.command('add-children', 'iot_device_children_add') - cmd_group.command('remove-children', 'iot_device_children_remove') - cmd_group.command('list-children', 'iot_device_children_list') - cmd_group.command('get-parent', 'iot_device_get_parent') - cmd_group.command('set-parent', 'iot_device_set_parent') - - with self.command_group('iot hub module-identity', command_type=iothub_ops) as cmd_group: - cmd_group.command('create', 'iot_device_module_create') - cmd_group.command('show', 'iot_device_module_show') - cmd_group.command('list', 'iot_device_module_list') - cmd_group.command('delete', 'iot_device_module_delete') - cmd_group.generic_update_command('update', getter_name='iot_device_module_show', - setter_name='iot_device_module_update') - - cmd_group.command('show-connection-string', 'iot_get_module_connection_string') - - with self.command_group('iot hub module-twin', command_type=iothub_ops) as cmd_group: - cmd_group.command('show', 'iot_device_module_twin_show') - cmd_group.command('replace', 'iot_device_module_twin_replace') - cmd_group.generic_update_command('update', getter_name='iot_device_module_twin_show', - setter_name='iot_device_module_twin_update') - - with self.command_group('iot hub device-twin', command_type=iothub_ops) as cmd_group: - cmd_group.command('show', 'iot_device_twin_show') - cmd_group.command('replace', 'iot_device_twin_replace') - cmd_group.generic_update_command('update', getter_name='iot_device_twin_show', - setter_name='iot_device_twin_update') - - with self.command_group('iot hub configuration', command_type=iothub_ops) as cmd_group: - cmd_group.command('show-metric', 'iot_hub_configuration_metric_show') - cmd_group.command('create', 'iot_hub_configuration_create') - cmd_group.command('show', 'iot_hub_configuration_show') - cmd_group.command('list', 'iot_hub_configuration_list') - cmd_group.command('delete', 'iot_hub_configuration_delete') - cmd_group.generic_update_command('update', getter_name='iot_hub_configuration_show', - setter_name='iot_hub_configuration_update') - - with self.command_group('iot hub distributed-tracing', command_type=iothub_ops, is_preview=True) as cmd_group: - cmd_group.command('show', 'iot_hub_distributed_tracing_show') - cmd_group.command('update', 'iot_hub_distributed_tracing_update') - - with self.command_group('iot edge', command_type=iothub_ops) as cmd_group: - cmd_group.command('set-modules', 'iot_edge_set_modules') - - with self.command_group('iot edge deployment', command_type=iothub_ops) as cmd_group: - cmd_group.command('show-metric', 'iot_edge_deployment_metric_show') - cmd_group.command('create', 'iot_edge_deployment_create') - cmd_group.command('show', 'iot_hub_configuration_show') - cmd_group.command('list', 'iot_edge_deployment_list') - cmd_group.command('delete', 'iot_hub_configuration_delete') - cmd_group.generic_update_command('update', getter_name='iot_hub_configuration_show', - setter_name='iot_hub_configuration_update') - - with self.command_group('iot device', command_type=iothub_ops) as cmd_group: - cmd_group.command('send-d2c-message', 'iot_device_send_message') - cmd_group.command('simulate', 'iot_simulate_device') - cmd_group.command('upload-file', 'iot_device_upload_file') - - with self.command_group('iot device c2d-message', command_type=iothub_ops) as cmd_group: - cmd_group.command('complete', 'iot_c2d_message_complete') - cmd_group.command('abandon', 'iot_c2d_message_abandon') - cmd_group.command('reject', 'iot_c2d_message_reject') - cmd_group.command('receive', 'iot_c2d_message_receive') - cmd_group.command('send', 'iot_c2d_message_send') - - with self.command_group('iot dps enrollment', command_type=iotdps_ops) as cmd_group: - cmd_group.command('create', 'iot_dps_device_enrollment_create') - cmd_group.command('list', 'iot_dps_device_enrollment_list') - cmd_group.command('show', 'iot_dps_device_enrollment_get') - cmd_group.command('update', 'iot_dps_device_enrollment_update') - cmd_group.command('delete', 'iot_dps_device_enrollment_delete') - - with self.command_group('iot dps enrollment-group', command_type=iotdps_ops) as cmd_group: - cmd_group.command('create', 'iot_dps_device_enrollment_group_create') - cmd_group.command('list', 'iot_dps_device_enrollment_group_list') - cmd_group.command('show', 'iot_dps_device_enrollment_group_get') - cmd_group.command('update', 'iot_dps_device_enrollment_group_update') - cmd_group.command('delete', 'iot_dps_device_enrollment_group_delete') - - with self.command_group('iot dps registration', command_type=iotdps_ops) as cmd_group: - cmd_group.command('list', 'iot_dps_registration_list') - cmd_group.command('show', 'iot_dps_registration_get') - cmd_group.command('delete', 'iot_dps_registration_delete') - - with self.command_group('iotcentral app', command_type=iotcentral_ops) as cmd_group: - cmd_group.command('monitor-events', 'iot_central_monitor_events', - deprecate_info='az iot central app monitor-events') - - with self.command_group('iotcentral device-twin', command_type=iotcentral_ops) as cmd_group: - cmd_group.command('show', 'iot_central_device_show', - deprecate_info='az iot central device-twin') - - with self.command_group('iot central app', command_type=iotcentral_ops) as cmd_group: - cmd_group.command('monitor-events', 'iot_central_monitor_events') - - with self.command_group('iot central device-twin', command_type=iotcentral_ops) as cmd_group: - cmd_group.command('show', 'iot_central_device_show') - - with self.command_group('iot dt', command_type=iotdigitaltwin_ops, is_preview=True) as cmd_group: - cmd_group.command('list-interfaces', 'iot_digitaltwin_interface_list') - cmd_group.command('list-properties', 'iot_digitaltwin_properties_list') - cmd_group.command('update-property', 'iot_digitaltwin_property_update') - cmd_group.command('invoke-command', 'iot_digitaltwin_invoke_command') - cmd_group.command('monitor-events', 'iot_digitaltwin_monitor_events') - cmd_group.command('list-commands', 'iot_digitaltwin_command_list') - - with self.command_group('iot pnp interface', command_type=iotpnp_ops, is_preview=True) as cmd_group: - cmd_group.command('show', 'iot_pnp_interface_show') - cmd_group.command('list', 'iot_pnp_interface_list') - cmd_group.command('create', 'iot_pnp_interface_create') - cmd_group.command('publish', 'iot_pnp_interface_publish') - cmd_group.command('delete', 'iot_pnp_interface_delete') - cmd_group.command('update', 'iot_pnp_interface_update') - - with self.command_group('iot pnp capability-model', command_type=iotpnp_ops, is_preview=True) as cmd_group: - cmd_group.command('show', 'iot_pnp_model_show') - cmd_group.command('list', 'iot_pnp_model_list') - cmd_group.command('create', 'iot_pnp_model_create') - cmd_group.command('publish', 'iot_pnp_model_publish') - cmd_group.command('delete', 'iot_pnp_model_delete') - cmd_group.command('update', 'iot_pnp_model_update') + with self.command_group("iot hub", command_type=iothub_ops) as cmd_group: + cmd_group.command("query", "iot_query") + cmd_group.command("invoke-device-method", "iot_device_method") + cmd_group.command("invoke-module-method", "iot_device_module_method") + cmd_group.command("generate-sas-token", "iot_get_sas_token") + cmd_group.command("monitor-events", "iot_hub_monitor_events") + cmd_group.command("monitor-feedback", "iot_hub_monitor_feedback") + + with self.command_group( + "iot hub device-identity", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("create", "iot_device_create") + cmd_group.command("show", "iot_device_show") + cmd_group.command("list", "iot_device_list") + cmd_group.command("delete", "iot_device_delete") + cmd_group.generic_update_command( + "update", getter_name="iot_device_show", setter_name="iot_device_update" + ) + + cmd_group.command("show-connection-string", "iot_get_device_connection_string") + cmd_group.command("import", "iot_device_import") + cmd_group.command("export", "iot_device_export") + cmd_group.command("add-children", "iot_device_children_add") + cmd_group.command("remove-children", "iot_device_children_remove") + cmd_group.command("list-children", "iot_device_children_list") + cmd_group.command("get-parent", "iot_device_get_parent") + cmd_group.command("set-parent", "iot_device_set_parent") + + with self.command_group( + "iot hub module-identity", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("create", "iot_device_module_create") + cmd_group.command("show", "iot_device_module_show") + cmd_group.command("list", "iot_device_module_list") + cmd_group.command("delete", "iot_device_module_delete") + cmd_group.generic_update_command( + "update", + getter_name="iot_device_module_show", + setter_name="iot_device_module_update", + ) + + cmd_group.command("show-connection-string", "iot_get_module_connection_string") + + with self.command_group( + "iot hub module-twin", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("show", "iot_device_module_twin_show") + cmd_group.command("replace", "iot_device_module_twin_replace") + cmd_group.generic_update_command( + "update", + getter_name="iot_device_module_twin_show", + setter_name="iot_device_module_twin_update", + ) + + with self.command_group( + "iot hub device-twin", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("show", "iot_device_twin_show") + cmd_group.command("replace", "iot_device_twin_replace") + cmd_group.generic_update_command( + "update", + getter_name="iot_device_twin_show", + setter_name="iot_device_twin_update", + ) + + with self.command_group( + "iot hub configuration", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("show-metric", "iot_hub_configuration_metric_show") + cmd_group.command("create", "iot_hub_configuration_create") + cmd_group.command("show", "iot_hub_configuration_show") + cmd_group.command("list", "iot_hub_configuration_list") + cmd_group.command("delete", "iot_hub_configuration_delete") + cmd_group.generic_update_command( + "update", + getter_name="iot_hub_configuration_show", + setter_name="iot_hub_configuration_update", + ) + + with self.command_group( + "iot hub distributed-tracing", command_type=iothub_ops, is_preview=True + ) as cmd_group: + cmd_group.command("show", "iot_hub_distributed_tracing_show") + cmd_group.command("update", "iot_hub_distributed_tracing_update") + + with self.command_group("iot edge", command_type=iothub_ops) as cmd_group: + cmd_group.command("set-modules", "iot_edge_set_modules") + + with self.command_group( + "iot edge deployment", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("show-metric", "iot_edge_deployment_metric_show") + cmd_group.command("create", "iot_edge_deployment_create") + cmd_group.command("show", "iot_hub_configuration_show") + cmd_group.command("list", "iot_edge_deployment_list") + cmd_group.command("delete", "iot_hub_configuration_delete") + cmd_group.generic_update_command( + "update", + getter_name="iot_hub_configuration_show", + setter_name="iot_hub_configuration_update", + ) + + with self.command_group("iot device", command_type=iothub_ops) as cmd_group: + cmd_group.command("send-d2c-message", "iot_device_send_message") + cmd_group.command("simulate", "iot_simulate_device") + cmd_group.command("upload-file", "iot_device_upload_file") + + with self.command_group( + "iot device c2d-message", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("complete", "iot_c2d_message_complete") + cmd_group.command("abandon", "iot_c2d_message_abandon") + cmd_group.command("reject", "iot_c2d_message_reject") + cmd_group.command("receive", "iot_c2d_message_receive") + cmd_group.command("send", "iot_c2d_message_send") + + with self.command_group("iot dps enrollment", command_type=iotdps_ops) as cmd_group: + cmd_group.command("create", "iot_dps_device_enrollment_create") + cmd_group.command("list", "iot_dps_device_enrollment_list") + cmd_group.command("show", "iot_dps_device_enrollment_get") + cmd_group.command("update", "iot_dps_device_enrollment_update") + cmd_group.command("delete", "iot_dps_device_enrollment_delete") + + with self.command_group( + "iot dps enrollment-group", command_type=iotdps_ops + ) as cmd_group: + cmd_group.command("create", "iot_dps_device_enrollment_group_create") + cmd_group.command("list", "iot_dps_device_enrollment_group_list") + cmd_group.command("show", "iot_dps_device_enrollment_group_get") + cmd_group.command("update", "iot_dps_device_enrollment_group_update") + cmd_group.command("delete", "iot_dps_device_enrollment_group_delete") + + with self.command_group( + "iot dps registration", command_type=iotdps_ops + ) as cmd_group: + cmd_group.command("list", "iot_dps_registration_list") + cmd_group.command("show", "iot_dps_registration_get") + cmd_group.command("delete", "iot_dps_registration_delete") + + with self.command_group("iotcentral app", command_type=iotcentral_ops) as cmd_group: + cmd_group.command( + "monitor-events", + "iot_central_monitor_events", + deprecate_info="az iot central app monitor-events", + ) + + with self.command_group( + "iotcentral device-twin", command_type=iotcentral_ops + ) as cmd_group: + cmd_group.command( + "show", + "iot_central_device_show", + deprecate_info="az iot central device-twin", + ) + + with self.command_group( + "iot central app", command_type=iotcentral_ops + ) as cmd_group: + cmd_group.command("monitor-events", "iot_central_monitor_events") + cmd_group.command( + "validate-messages", "iot_central_validate_messages", is_preview=True + ) + + with self.command_group( + "iot central device-twin", command_type=iotcentral_ops + ) as cmd_group: + cmd_group.command("show", "iot_central_device_show") + + with self.command_group( + "iot dt", command_type=iotdigitaltwin_ops, is_preview=True + ) as cmd_group: + cmd_group.command("list-interfaces", "iot_digitaltwin_interface_list") + cmd_group.command("list-properties", "iot_digitaltwin_properties_list") + cmd_group.command("update-property", "iot_digitaltwin_property_update") + cmd_group.command("invoke-command", "iot_digitaltwin_invoke_command") + cmd_group.command("monitor-events", "iot_digitaltwin_monitor_events") + cmd_group.command("list-commands", "iot_digitaltwin_command_list") + + with self.command_group( + "iot pnp interface", command_type=iotpnp_ops, is_preview=True + ) as cmd_group: + cmd_group.command("show", "iot_pnp_interface_show") + cmd_group.command("list", "iot_pnp_interface_list") + cmd_group.command("create", "iot_pnp_interface_create") + cmd_group.command("publish", "iot_pnp_interface_publish") + cmd_group.command("delete", "iot_pnp_interface_delete") + cmd_group.command("update", "iot_pnp_interface_update") + + with self.command_group( + "iot pnp capability-model", command_type=iotpnp_ops, is_preview=True + ) as cmd_group: + cmd_group.command("show", "iot_pnp_model_show") + cmd_group.command("list", "iot_pnp_model_list") + cmd_group.command("create", "iot_pnp_model_create") + cmd_group.command("publish", "iot_pnp_model_publish") + cmd_group.command("delete", "iot_pnp_model_delete") + cmd_group.command("update", "iot_pnp_model_update") diff --git a/azext_iot/constants.py b/azext_iot/constants.py index f70a4da2d..4054eddc6 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.1" +VERSION = "0.9.2" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 2e0daa5be..5c30a042a 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -8,18 +8,21 @@ from azext_iot._factory import _bind_sdk from azext_iot.common._azure import get_iot_hub_token_from_central_app_id from azext_iot.common.shared import SdkType -from azext_iot.common.utility import (unpack_msrest_error, init_monitoring) +from azext_iot.common.utility import unpack_msrest_error, init_monitoring from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication +from azext_iot.operations import events3 def find_between(s, start, end): return (s.split(start))[1].split(end)[0] -def iot_central_device_show(cmd, device_id, app_id, central_api_uri='api.azureiotcentral.com'): +def iot_central_device_show( + cmd, device_id, app_id, central_api_uri="api.azureiotcentral.com" +): sasToken = get_iot_hub_token_from_central_app_id(cmd, app_id, central_api_uri) - endpoint = find_between(sasToken, 'SharedAccessSignature sr=', '&sig=') - target = {'entity': endpoint} + endpoint = find_between(sasToken, "SharedAccessSignature sr=", "&sig=") + target = {"entity": endpoint} auth = BasicSasTokenAuthentication(sas_token=sasToken) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk, auth=auth) try: @@ -28,21 +31,93 @@ def iot_central_device_show(cmd, device_id, app_id, central_api_uri='api.azureio raise CLIError(unpack_msrest_error(e)) -def iot_central_monitor_events(cmd, app_id, device_id=None, consumer_group='$Default', timeout=300, enqueued_time=None, - repair=False, properties=None, yes=False, central_api_uri='api.azureiotcentral.com'): +def iot_central_validate_messages( + cmd, + app_id, + device_id=None, + simulate_errors=False, + consumer_group="$Default", + timeout=300, + enqueued_time=None, + repair=False, + properties=None, + yes=False, + central_api_uri="api.azureiotcentral.com", +): + _events3_runner( + cmd, + app_id, + device_id, + True, + simulate_errors, + consumer_group, + timeout, + enqueued_time, + repair, + properties, + yes, + central_api_uri, + ) - (enqueued_time, properties, timeout, output) = init_monitoring(cmd, timeout, properties, enqueued_time, repair, yes) - import importlib +def iot_central_monitor_events( + cmd, + app_id, + device_id=None, + consumer_group="$Default", + timeout=300, + enqueued_time=None, + repair=False, + properties=None, + yes=False, + central_api_uri="api.azureiotcentral.com", +): + _events3_runner( + cmd, + app_id, + device_id, + False, + False, + consumer_group, + timeout, + enqueued_time, + repair, + properties, + yes, + central_api_uri, + ) - events3 = importlib.import_module('azext_iot.operations.events3._events') - builders = importlib.import_module('azext_iot.operations.events3._builders') - eventHubTarget = builders.EventTargetBuilder().build_central_event_hub_target(cmd, app_id, central_api_uri) - events3.executor(eventHubTarget, - consumer_group=consumer_group, - enqueued_time=enqueued_time, - properties=properties, - timeout=timeout, - device_id=device_id, - output=output) +def _events3_runner( + cmd, + app_id, + device_id, + validate_messages, + simulate_errors, + consumer_group, + timeout, + enqueued_time, + repair, + properties, + yes, + central_api_uri, +): + (enqueued_time, properties, timeout, output) = init_monitoring( + cmd, timeout, properties, enqueued_time, repair, yes + ) + + eventHubTarget = events3.EventTargetBuilder().build_central_event_hub_target( + cmd, app_id, central_api_uri + ) + + events3.executor( + eventHubTarget, + consumer_group=consumer_group, + enqueued_time=enqueued_time, + properties=properties, + timeout=timeout, + device_id=device_id, + output=output, + validate_messages=validate_messages, + simulate_errors=simulate_errors, + ) diff --git a/azext_iot/operations/events3/__init__.py b/azext_iot/operations/events3/__init__.py index 55614acbf..5087691d6 100644 --- a/azext_iot/operations/events3/__init__.py +++ b/azext_iot/operations/events3/__init__.py @@ -3,3 +3,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from ._builders import EventTargetBuilder +from ._events import executor, send_c2d_message, monitor_feedback + +__all__ = ["EventTargetBuilder", "executor", "send_c2d_message", "monitor_feedback"] diff --git a/azext_iot/operations/events3/_events.py b/azext_iot/operations/events3/_events.py index 15f9bac36..278ed22ec 100644 --- a/azext_iot/operations/events3/_events.py +++ b/azext_iot/operations/events3/_events.py @@ -15,8 +15,9 @@ from uuid import uuid4 from knack.log import get_logger from azext_iot.constants import VERSION, USER_AGENT -from azext_iot.common.utility import parse_entity, unicode_binary_map, process_json_arg +from azext_iot.common.utility import process_json_arg from azext_iot.operations.events3._builders import AmqpBuilder +from azext_iot.operations.events3._parser import Event3Parser # To provide amqp frame trace DEBUG = False @@ -35,6 +36,8 @@ def executor( devices=None, interface_name=None, pnp_context=None, + validate_messages=False, + simulate_errors=False, ): coroutines = [] @@ -51,6 +54,8 @@ def executor( devices, interface_name, pnp_context, + validate_messages, + simulate_errors, ) ) @@ -106,6 +111,8 @@ async def initiate_event_monitor( devices=None, interface_name=None, pnp_context=None, + validate_messages=False, + simulate_errors=False, ): def _get_conn_props(): properties = {} @@ -146,6 +153,8 @@ def _get_conn_props(): devices=devices, interface_name=interface_name, pnp_context=pnp_context, + validate_messages=validate_messages, + simulate_errors=simulate_errors, ) ) return await asyncio.gather(*coroutines, return_exceptions=True) @@ -167,6 +176,8 @@ async def monitor_events( devices=None, interface_name=None, pnp_context=None, + validate_messages=False, + simulate_errors=False, ): source = uamqp.address.Source( "amqps://{}/{}/ConsumerGroups/{}/Partitions/{}".format( @@ -177,84 +188,14 @@ async def monitor_events( bytes("amqp.annotation.x-opt-enqueuedtimeutc > " + str(enqueuedtimeutc), "utf8") ) - def _output_msg_kpi(msg): - origin = str(msg.annotations.get(b"iothub-connection-device-id"), "utf8") - if device_id and device_id != origin: - if "*" in device_id or "?" in device_id: - regex = ( - re.escape(device_id).replace("\\*", ".*").replace("\\?", ".") + "$" - ) - if not re.match(regex, origin): - return - else: - return - if devices and origin not in devices: - return - - event_source = {"event": {}} - event_source["event"]["origin"] = origin - payload = "" - - if pnp_context: - msg_interface_name = str( - msg.annotations.get(b"iothub-interface-name"), "utf8" - ) - if not msg_interface_name: - return - - if interface_name: - if msg_interface_name != interface_name: - return - - event_source["event"]["interface"] = msg_interface_name - - data = msg.get_data() - if data: - payload = str(next(data), "utf8") - - system_props = unicode_binary_map(parse_entity(msg.properties, True)) - - ct = content_type - if not ct: - ct = system_props["content_type"] if "content_type" in system_props else "" - - if ct and "application/json" in ct.lower(): - try: - payload = json.loads( - re.compile(r"(\\r\\n)+|\\r+|\\n+").sub("", payload) - ) - except Exception: - # We don't want to crash the monitor if JSON parsing fails - pass - - event_source["event"]["payload"] = payload - - if "anno" in properties or "all" in properties: - event_source["event"]["annotations"] = unicode_binary_map(msg.annotations) - if "sys" in properties or "all" in properties: - if not event_source["event"].get("properties"): - event_source["event"]["properties"] = {} - event_source["event"]["properties"]["system"] = system_props - if "app" in properties or "all" in properties: - if not event_source["event"].get("properties"): - event_source["event"]["properties"] = {} - app_prop = ( - msg.application_properties if msg.application_properties else None - ) - - if app_prop: - event_source["event"]["properties"]["application"] = unicode_binary_map( - app_prop - ) - if output.lower() == "json": - dump = json.dumps(event_source, indent=4) - else: - dump = yaml.safe_dump(event_source, default_flow_style=False) - six.print_(dump, flush=True) - exp_cancelled = False receive_client = uamqp.ReceiveClientAsync( - source, auth=auth, timeout=timeout, prefetch=0, client_name=_get_container_id(), debug=DEBUG + source, + auth=auth, + timeout=timeout, + prefetch=0, + client_name=_get_container_id(), + debug=DEBUG, ) try: @@ -262,7 +203,18 @@ def _output_msg_kpi(msg): await receive_client.open_async(connection=connection) async for msg in receive_client.receive_messages_iter_async(): - _output_msg_kpi(msg) + _output_msg_kpi( + msg, + device_id, + devices, + pnp_context, + interface_name, + content_type, + properties, + output, + validate_messages, + simulate_errors, + ) except asyncio.CancelledError: exp_cancelled = True @@ -337,7 +289,9 @@ def send_c2d_message( endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target(target) endpoint_with_op = endpoint + operation client = uamqp.SendClient( - target="amqps://" + endpoint_with_op, client_name=_get_container_id(), debug=DEBUG + target="amqps://" + endpoint_with_op, + client_name=_get_container_id(), + debug=DEBUG, ) client.queue_message(message) result = client.send_all_messages() @@ -401,3 +355,51 @@ def handle_msg(msg): def _get_container_id(): return "{}/{}".format(USER_AGENT, str(uuid4())) + + +def _output_msg_kpi( + msg, + device_id, + devices, + pnp_context, + interface_name, + content_type, + properties, + output, + validate_messages, + simulate_errors, +): + parser = Event3Parser() + origin_device_id = parser.parse_device_id(msg) + + if not _should_process_device(origin_device_id, device_id, devices): + return + + parsed_msg = parser.parse_message( + msg, pnp_context, interface_name, properties, content_type, simulate_errors + ) + + if output.lower() == "json": + dump = json.dumps(parsed_msg, indent=4) + else: + dump = yaml.safe_dump(parsed_msg, default_flow_style=False) + + if validate_messages: + parser.write_logs() + + if not validate_messages: + six.print_(dump, flush=True) + + +def _should_process_device(origin_device_id, device_id, devices): + if device_id and device_id != origin_device_id: + if "*" in device_id or "?" in device_id: + regex = re.escape(device_id).replace("\\*", ".*").replace("\\?", ".") + "$" + if not re.match(regex, origin_device_id): + return False + else: + return False + if devices and origin_device_id not in devices: + return False + + return True diff --git a/azext_iot/operations/events3/_parser.py b/azext_iot/operations/events3/_parser.py new file mode 100644 index 000000000..38f7ebd3c --- /dev/null +++ b/azext_iot/operations/events3/_parser.py @@ -0,0 +1,259 @@ +# 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 random +import re +import json + +from knack.log import get_logger +from uamqp.message import Message +from azext_iot.common.utility import parse_entity, unicode_binary_map + +SUPPORTED_ENCODINGS = ["utf-8"] +DEVICE_ID_IDENTIFIER = b"iothub-connection-device-id" +INTERFACE_NAME_IDENTIFIER = b"iothub-interface-name" +random.seed(0) + + +class Event3Parser(object): + _info = [] + _warnings = [] + _errors = [] + _logger = get_logger(__name__) + + def __init__(self, logger=None): + if logger: + self._logger = logger + + def parse_message( + self, + message: Message, + pnp_context: bool, + interface_name: str, + properties: dict, + content_type_hint: str, + simulate_errors: bool, + ) -> dict: + self._reset_issues() + create_encoding_error = False + create_custom_header_warning = False + create_payload_error = False + + if not properties: + properties = {} # guard against None being passed in + + i = random.randint(1, 3) + if simulate_errors and i == 1: + create_encoding_error = True + if simulate_errors and i == 2: + create_custom_header_warning = True + if simulate_errors and i == 3: + create_payload_error = True + + system_properties = self._parse_system_properties(message) + + self._parse_content_encoding(message, system_properties, create_encoding_error) + + event = {} + + origin_device_id = self.parse_device_id(message) + event["origin"] = origin_device_id + + content_type = self._parse_content_type( + content_type_hint, + system_properties, + origin_device_id, + create_custom_header_warning, + ) + + if pnp_context: + message_interface_name = self._parse_interface_name( + message, pnp_context, interface_name, origin_device_id + ) + + event["interface"] = message_interface_name + + if properties: + event["properties"] = {} + + if "anno" in properties or "all" in properties: + annotations = self._parse_annotations(message) + event["properties"]["annotations"] = annotations + + if system_properties and ("sys" in properties or "all" in properties): + event["properties"]["system"] = system_properties + + if "app" in properties or "all" in properties: + application_properties = self._parse_application_properties(message) + event["properties"]["application"] = application_properties + + payload = self._parse_payload( + message, origin_device_id, content_type, create_payload_error + ) + + event["payload"] = payload + + event_source = {"event": event} + + return event_source + + def parse_device_id(self, message: Message) -> str: + try: + return str(message.annotations.get(DEVICE_ID_IDENTIFIER), "utf8") + except Exception: + self._errors.append("Device id not found in message: {}".format(message)) + + def write_logs(self) -> None: + for error in self._errors: + self._logger.error("[Error] " + error) + + for warning in self._warnings: + self._logger.warn("[Warning] " + warning) + + for info in self._info: + self._logger.info("[Info] " + info) + + def _reset_issues(self) -> None: + self._info = [] + self._warnings = [] + self._errors = [] + + def _parse_interface_name( + self, message: Message, pnp_context, interface_name, origin_device_id + ) -> str: + message_interface_name = "" + + try: + message_interface_name = str( + message.annotations.get(INTERFACE_NAME_IDENTIFIER), "utf8" + ) + except Exception: + self._errors.append( + "Unable to parse interface_name given a pnp_device. {}. " + "message: {}".format(origin_device_id, message) + ) + + if interface_name != message_interface_name: + self._errors.append( + "Inteface name mismatch. {}. " + "Expected: {}, Actual: {}".format( + origin_device_id, interface_name, message_interface_name + ) + ) + + return message_interface_name + + def _parse_system_properties(self, message: Message): + try: + return unicode_binary_map(parse_entity(message.properties, True)) + except Exception: + self._errors.append( + "Failed to parse system_properties for message {message}.".format( + message + ) + ) + return {} + + def _parse_content_encoding( + self, message: Message, system_properties, create_encoding_error + ) -> str: + content_encoding = "" + + if "content_encoding" in system_properties: + content_encoding = system_properties["content_encoding"] + + if not content_encoding: + self._errors.append("No encoding found for message: {}".format(message)) + return None + + if create_encoding_error: + content_encoding = "Some Random Encoding" + + if "utf-8" not in content_encoding.lower(): + self._errors.append( + "Unsupported encoding detected: '{}'. " + "The currently supported encodings are: {}. " + "System_properties: {}.".format( + content_encoding, SUPPORTED_ENCODINGS, system_properties + ) + ) + return None + + return content_encoding + + def _parse_content_type( + self, + content_type_hint, + system_properties, + origin_device_id, + create_custom_header_warning, + ) -> str: + content_type = "" + if content_type_hint: + content_type = content_type_hint + elif "content_type" in system_properties: + content_type = system_properties["content_type"] + + if create_custom_header_warning: + content_type = "Some Random Custom Header" + + if not content_type: + self._warnings.append( + "Content type not found in system_properties. " + "System_properties: {}".format(system_properties) + ) + + return content_type + + def _parse_payload( + self, message: Message, origin_device_id, content_type, create_payload_error + ): + payload = "" + data = message.get_data() + + if data: + payload = str(next(data), "utf8") + + if create_payload_error: + payload = "Some Random Payload" + + if "application/json" not in content_type.lower(): + self._warnings.append( + "Content type not supported. " + "Content type found: {}. " + "Content type expected: application/json. " + "DeviceId: {}".format(content_type, origin_device_id) + ) + else: + try: + payload = json.loads( + re.compile(r"(\\r\\n)+|\\r+|\\n+").sub("", payload) + ) + except Exception: + self._errors.append( + "Invalid JSON format. " + "DeviceId: {}, Raw payload {}".format(origin_device_id, payload) + ) + + return payload + + def _parse_annotations(self, message: Message): + try: + return unicode_binary_map(message.annotations) + except Exception: + self._warnings.append( + "Unable to decode message.annotations: {}".format(message.annotations) + ) + + def _parse_application_properties(self, message: Message): + try: + return unicode_binary_map(message.application_properties) + except Exception: + self._warnings.append( + "Unable to decode message.application_properties: {}".format( + message.application_properties + ) + ) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index b4f1a4c12..ccda89777 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -9,25 +9,27 @@ import six from knack.log import get_logger from knack.util import CLIError -from azext_iot.constants import (EXTENSION_ROOT, - DEVICE_DEVICESCOPE_PREFIX, - TRACING_PROPERTY, - TRACING_ALLOWED_FOR_LOCATION, - TRACING_ALLOWED_FOR_SKU) +from azext_iot.constants import ( + EXTENSION_ROOT, + DEVICE_DEVICESCOPE_PREFIX, + TRACING_PROPERTY, + TRACING_ALLOWED_FOR_LOCATION, + TRACING_ALLOWED_FOR_SKU, +) from azext_iot.common.sas_token_auth import SasTokenAuthentication -from azext_iot.common.shared import (DeviceAuthType, - SdkType, - ProtocolType, - ConfigType) +from azext_iot.common.shared import DeviceAuthType, SdkType, ProtocolType, ConfigType from azext_iot.common._azure import get_iot_hub_connection_string -from azext_iot.common.utility import (shell_safe_json_parse, - read_file_content, - validate_key_value_pairs, - url_encode_dict, - unpack_msrest_error, - init_monitoring, - process_json_arg) +from azext_iot.common.utility import ( + shell_safe_json_parse, + read_file_content, + validate_key_value_pairs, + url_encode_dict, + unpack_msrest_error, + init_monitoring, + process_json_arg, +) from azext_iot._factory import SdkResolver, CloudError +from azext_iot.operations import events3 from azext_iot.operations.generic import _execute_query, _process_top @@ -36,9 +38,14 @@ # Query -def iot_query(cmd, query_command, hub_name=None, top=None, resource_group_name=None, login=None): + +def iot_query( + cmd, query_command, hub_name=None, top=None, resource_group_name=None, login=None +): top = _process_top(top) - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -54,8 +61,13 @@ def iot_query(cmd, query_command, hub_name=None, top=None, resource_group_name=N # Device -def iot_device_show(cmd, device_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + +def iot_device_show( + cmd, device_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_device_show(target, device_id) @@ -64,80 +76,132 @@ def _iot_device_show(target, device_id): service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - device = service_sdk.registry_manager.get_device(id=device_id, raw=True).response.json() - device['hub'] = target.get('entity') + device = service_sdk.registry_manager.get_device( + id=device_id, raw=True + ).response.json() + device["hub"] = target.get("entity") return device except CloudError as e: raise CLIError(unpack_msrest_error(e)) -def iot_device_list(cmd, hub_name=None, top=1000, edge_enabled=False, resource_group_name=None, login=None): - query = 'select * from devices where capabilities.iotEdge = true' if edge_enabled else 'select * from devices' +def iot_device_list( + cmd, + hub_name=None, + top=1000, + edge_enabled=False, + resource_group_name=None, + login=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) if not result: logger.info('No registered devices found on hub "%s".', hub_name) return result -def iot_device_create(cmd, device_id, hub_name=None, edge_enabled=False, - auth_method='shared_private_key', primary_thumbprint=None, - secondary_thumbprint=None, status='enabled', status_reason=None, - valid_days=None, output_dir=None, set_parent_id=None, add_children=None, - force=False, resource_group_name=None, login=None): - - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_create( + cmd, + device_id, + hub_name=None, + edge_enabled=False, + auth_method="shared_private_key", + primary_thumbprint=None, + secondary_thumbprint=None, + status="enabled", + status_reason=None, + valid_days=None, + output_dir=None, + set_parent_id=None, + add_children=None, + force=False, + resource_group_name=None, + login=None, +): + + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) deviceScope = None if edge_enabled: if auth_method != DeviceAuthType.shared_private_key.name: - raise CLIError('currently edge devices are limited to symmetric key auth') + raise CLIError("currently edge devices are limited to symmetric key auth") if add_children: - for non_edge_device_id in add_children.split(','): + for non_edge_device_id in add_children.split(","): nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) _validate_nonedge_device(nonedge_device) - _validate_parent_child_relation(nonedge_device, '-', force) + _validate_parent_child_relation(nonedge_device, "-", force) else: if set_parent_id: edge_device = _iot_device_show(target, set_parent_id) _validate_edge_device(edge_device) - deviceScope = edge_device['deviceScope'] + 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): - raise CLIError("certificate output directory of '{}' does not exist.".format(output_dir)) + raise CLIError( + "certificate output directory of '{}' does not exist.".format( + output_dir + ) + ) cert = _create_self_signed_cert(device_id, valid_days, output_dir) primary_thumbprint = cert["thumbprint"] try: - device = _assemble_device(device_id, auth_method, edge_enabled, primary_thumbprint, - secondary_thumbprint, status, status_reason, deviceScope) - output = service_sdk.registry_manager.create_or_update_device(id=device_id, device=device) + device = _assemble_device( + device_id, + auth_method, + edge_enabled, + primary_thumbprint, + secondary_thumbprint, + status, + status_reason, + deviceScope, + ) + output = service_sdk.registry_manager.create_or_update_device( + id=device_id, device=device + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) if add_children: - for non_edge_device_id in add_children.split(','): + for non_edge_device_id in add_children.split(","): nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) _update_nonedge_devicescope(target, nonedge_device, output.device_scope) return output -def _assemble_device(device_id, auth_method, edge_enabled, pk=None, sk=None, - status="enabled", status_reason=None, device_scope=None): - from azext_iot.sdk.iothub.service.models import ( - DeviceCapabilities, - Device - ) +def _assemble_device( + device_id, + auth_method, + edge_enabled, + pk=None, + sk=None, + status="enabled", + status_reason=None, + device_scope=None, +): + from azext_iot.sdk.iothub.service.models import DeviceCapabilities, Device auth = _assemble_auth(auth_method, pk, sk) cap = DeviceCapabilities(iot_edge=edge_enabled) - device = Device(device_id=device_id, authentication=auth, - capabilities=cap, status=status, status_reason=status_reason, - device_scope=device_scope) + device = Device( + device_id=device_id, + authentication=auth, + capabilities=cap, + status=status, + status_reason=status_reason, + device_scope=device_scope, + ) return device @@ -145,33 +209,42 @@ def _assemble_auth(auth_method, pk, sk): from azext_iot.sdk.iothub.service.models import ( AuthenticationMechanism, SymmetricKey, - X509Thumbprint + X509Thumbprint, ) auth = None if auth_method in [DeviceAuthType.shared_private_key.name, "sas"]: auth = AuthenticationMechanism( - symmetric_key=SymmetricKey(primary_key=pk, secondary_key=sk), type="sas") + symmetric_key=SymmetricKey(primary_key=pk, secondary_key=sk), type="sas" + ) elif auth_method in [DeviceAuthType.x509_thumbprint.name, "selfSigned"]: if not pk: raise ValueError("primary thumbprint required with selfSigned auth") - auth = AuthenticationMechanism(x509_thumbprint=X509Thumbprint( - primary_thumbprint=pk, secondary_thumbprint=sk), type="selfSigned") + auth = AuthenticationMechanism( + x509_thumbprint=X509Thumbprint( + primary_thumbprint=pk, secondary_thumbprint=sk + ), + type="selfSigned", + ) elif auth_method in [DeviceAuthType.x509_ca.name, "certificateAuthority"]: auth = AuthenticationMechanism(type="certificateAuthority") else: - raise ValueError( - "Authorization method {} invalid.".format(auth_method)) + raise ValueError("Authorization method {} invalid.".format(auth_method)) return auth def _create_self_signed_cert(subject, valid_days, output_path=None): from azext_iot.common.certops import create_self_signed_certificate + return create_self_signed_certificate(subject, valid_days, output_path) -def iot_device_update(cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_update( + cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -182,9 +255,7 @@ def iot_device_update(cmd, device_id, parameters, hub_name=None, resource_group_ headers = {} headers["If-Match"] = '"{}"'.format(etag) return service_sdk.registry_manager.create_or_update_device( - id=device_id, - device=updated_device, - custom_headers=headers + id=device_id, device=updated_device, custom_headers=headers ) raise LookupError("device etag not found.") except CloudError as e: @@ -199,16 +270,28 @@ def _handle_device_update_params(parameters): if status not in possible_status: raise CLIError("status must be one of {}".format(possible_status)) - edge = parameters["capabilities"].get('iotEdge') + edge = parameters["capabilities"].get("iotEdge") if not isinstance(edge, bool): raise CLIError("capabilities.iotEdge is of type bool") auth, pk, sk = _parse_auth(parameters) - return _assemble_device(parameters["deviceId"], auth, edge, pk, sk, status, parameters.get("statusReason")) + return _assemble_device( + parameters["deviceId"], + auth, + edge, + pk, + sk, + status, + parameters.get("statusReason"), + ) -def iot_device_delete(cmd, device_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_delete( + cmd, device_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -219,7 +302,9 @@ def iot_device_delete(cmd, device_id, hub_name=None, resource_group_name=None, l if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - service_sdk.registry_manager.delete_device(id=device_id, custom_headers=headers) + service_sdk.registry_manager.delete_device( + id=device_id, custom_headers=headers + ) return raise LookupError("device etag not found") except CloudError as e: @@ -228,102 +313,160 @@ def iot_device_delete(cmd, device_id, hub_name=None, resource_group_name=None, l raise CLIError(err) -def iot_device_get_parent(cmd, device_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_get_parent( + cmd, device_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) child_device = _iot_device_show(target, device_id) _validate_nonedge_device(child_device) _validate_child_device(child_device) - device_scope = child_device['deviceScope'] - parent_device_id = device_scope[len(DEVICE_DEVICESCOPE_PREFIX):device_scope.rindex('-')] + device_scope = child_device["deviceScope"] + parent_device_id = device_scope[ + len(DEVICE_DEVICESCOPE_PREFIX) : device_scope.rindex("-") + ] return _iot_device_show(target, parent_device_id) -def iot_device_set_parent(cmd, device_id, parent_id, force=False, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_set_parent( + cmd, + device_id, + parent_id, + force=False, + hub_name=None, + resource_group_name=None, + login=None, +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) parent_device = _iot_device_show(target, parent_id) _validate_edge_device(parent_device) child_device = _iot_device_show(target, device_id) _validate_nonedge_device(child_device) - _validate_parent_child_relation(child_device, parent_device['deviceScope'], force) - _update_nonedge_devicescope(target, child_device, parent_device['deviceScope']) - - -def iot_device_children_add(cmd, device_id, child_list, force=False, hub_name=None, - resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + _validate_parent_child_relation(child_device, parent_device["deviceScope"], force) + _update_nonedge_devicescope(target, child_device, parent_device["deviceScope"]) + + +def iot_device_children_add( + cmd, + device_id, + child_list, + force=False, + hub_name=None, + resource_group_name=None, + login=None, +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) devices = [] edge_device = _iot_device_show(target, device_id) _validate_edge_device(edge_device) - for non_edge_device_id in child_list.split(','): + for non_edge_device_id in child_list.split(","): nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) _validate_nonedge_device(nonedge_device) - _validate_parent_child_relation(nonedge_device, edge_device['deviceScope'], force) + _validate_parent_child_relation( + nonedge_device, edge_device["deviceScope"], force + ) devices.append(nonedge_device) for device in devices: - _update_nonedge_devicescope(target, device, edge_device['deviceScope']) - - -def iot_device_children_remove(cmd, device_id, child_list=None, remove_all=False, hub_name=None, - resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + _update_nonedge_devicescope(target, device, edge_device["deviceScope"]) + + +def iot_device_children_remove( + cmd, + device_id, + child_list=None, + remove_all=False, + hub_name=None, + resource_group_name=None, + login=None, +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) devices = [] if remove_all: - result = _iot_device_children_list(cmd, device_id, hub_name, resource_group_name, login) + 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)) - for non_edge_device_id in ([str(x['deviceId']) for x in result]): + raise CLIError( + 'No registered child devices found for "{}" edge device.'.format( + device_id + ) + ) + for non_edge_device_id in [str(x["deviceId"]) for x in result]: nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) devices.append(nonedge_device) elif child_list: edge_device = _iot_device_show(target, device_id) _validate_edge_device(edge_device) - for non_edge_device_id in child_list.split(','): + for non_edge_device_id in child_list.split(","): nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) _validate_nonedge_device(nonedge_device) _validate_child_device(nonedge_device) - if nonedge_device['deviceScope'] == edge_device['deviceScope']: + if nonedge_device["deviceScope"] == edge_device["deviceScope"]: devices.append(nonedge_device) else: - raise CLIError('The entered child device "{}" isn\'t assigned as a child of edge device "{}"' - .format(non_edge_device_id.strip(), device_id)) + raise CLIError( + 'The entered child device "{}" isn\'t assigned as a child of edge device "{}"'.format( + non_edge_device_id.strip(), device_id + ) + ) else: - raise CLIError('Please specify comma-separated child list or use --remove-all to remove all children.') + raise CLIError( + "Please specify comma-separated child list or use --remove-all to remove all children." + ) for device in devices: _update_nonedge_devicescope(target, device) -def iot_device_children_list(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) +def iot_device_children_list( + 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]) + 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): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def _iot_device_children_list( + cmd, device_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) device = _iot_device_show(target, device_id) _validate_edge_device(device) - query = ('select * from devices where capabilities.iotEdge=false and deviceScope=\'{}\'' - .format(device['deviceScope'])) + query = "select * from devices where capabilities.iotEdge=false and deviceScope='{}'".format( + device["deviceScope"] + ) return iot_query(cmd, query, hub_name, None, resource_group_name, login=login) -def _update_nonedge_devicescope(target, nonedge_device, deviceScope=''): +def _update_nonedge_devicescope(target, nonedge_device, deviceScope=""): resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - nonedge_device['deviceScope'] = deviceScope - etag = nonedge_device.get('etag', None) + nonedge_device["deviceScope"] = deviceScope + etag = nonedge_device.get("etag", None) if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) service_sdk.registry_manager.create_or_update_device( - id=nonedge_device['deviceId'], + id=nonedge_device["deviceId"], device=nonedge_device, - custom_headers=headers + custom_headers=headers, ) return raise LookupError("device etag not found.") @@ -334,44 +477,73 @@ def _update_nonedge_devicescope(target, nonedge_device, deviceScope=''): def _validate_edge_device(device): - if not device['capabilities']['iotEdge']: - raise CLIError('The device "{}" should be edge device.'.format(device['deviceId'])) + if not device["capabilities"]["iotEdge"]: + raise CLIError( + 'The device "{}" should be edge device.'.format(device["deviceId"]) + ) def _validate_nonedge_device(device): - if device['capabilities']['iotEdge']: - raise CLIError('The entered child device "{}" should be non-edge device.'.format(device['deviceId'])) + if device["capabilities"]["iotEdge"]: + raise CLIError( + 'The entered child device "{}" should be non-edge device.'.format( + device["deviceId"] + ) + ) def _validate_child_device(device): - if 'deviceScope' not in device or device['deviceScope'] == '': - raise CLIError('Device "{}" doesn\'t support parent device functionality.'.format(device['deviceId'])) + if "deviceScope" not in device or device["deviceScope"] == "": + raise CLIError( + 'Device "{}" doesn\'t support parent device functionality.'.format( + device["deviceId"] + ) + ) def _validate_parent_child_relation(child_device, deviceScope, force): - if 'deviceScope' not in child_device or child_device['deviceScope'] == '': + if "deviceScope" not in child_device or child_device["deviceScope"] == "": return - if child_device['deviceScope'] != deviceScope: + if child_device["deviceScope"] != deviceScope: if not force: - raise CLIError('The entered device "{}" already has a parent device, please use \'--force\'' - ' to overwrite'.format(child_device['deviceId'])) + raise CLIError( + "The entered device \"{}\" already has a parent device, please use '--force'" + " to overwrite".format(child_device["deviceId"]) + ) return # Module -def iot_device_module_create(cmd, device_id, module_id, hub_name=None, auth_method='shared_private_key', - primary_thumbprint=None, secondary_thumbprint=None, valid_days=None, - output_dir=None, resource_group_name=None, login=None): + +def iot_device_module_create( + cmd, + device_id, + module_id, + hub_name=None, + auth_method="shared_private_key", + primary_thumbprint=None, + secondary_thumbprint=None, + valid_days=None, + output_dir=None, + resource_group_name=None, + login=None, +): 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): - raise CLIError("certificate output directory of '{}' does not exist.".format(output_dir)) + raise CLIError( + "certificate output directory of '{}' does not exist.".format( + output_dir + ) + ) cert = _create_self_signed_cert(module_id, valid_days, output_dir) primary_thumbprint = cert["thumbprint"] - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -381,9 +553,11 @@ def iot_device_module_create(cmd, device_id, module_id, hub_name=None, auth_meth module_id=module_id, auth_method=auth_method, pk=primary_thumbprint, - sk=secondary_thumbprint + sk=secondary_thumbprint, + ) + return service_sdk.registry_manager.create_or_update_module( + id=device_id, mid=module_id, module=module ) - return service_sdk.registry_manager.create_or_update_module(id=device_id, mid=module_id, module=module) except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -396,9 +570,18 @@ def _assemble_module(device_id, module_id, auth_method, pk=None, sk=None): return module -def iot_device_module_update(cmd, device_id, module_id, parameters, - hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_module_update( + cmd, + device_id, + module_id, + parameters, + hub_name=None, + resource_group_name=None, + login=None, +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -412,7 +595,7 @@ def iot_device_module_update(cmd, device_id, module_id, parameters, id=device_id, mid=module_id, module=updated_module, - custom_headers=headers + custom_headers=headers, ) raise LookupError("module etag not found.") except CloudError as e: @@ -428,13 +611,13 @@ def _handle_module_update_params(parameters): module_id=parameters["moduleId"], auth_method=auth, pk=pk, - sk=sk + sk=sk, ) def _parse_auth(parameters): valid_auth = ["sas", "selfSigned", "certificateAuthority"] - auth = parameters['authentication'].get('type') + 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 @@ -445,12 +628,18 @@ def _parse_auth(parameters): pk = parameters["authentication"]["x509Thumbprint"]["primaryThumbprint"] sk = parameters["authentication"]["x509Thumbprint"]["secondaryThumbprint"] if not any([pk, sk]): - raise CLIError("primary + secondary Thumbprint required with selfSigned auth") + raise CLIError( + "primary + secondary Thumbprint required with selfSigned auth" + ) return auth, pk, sk -def iot_device_module_list(cmd, device_id, hub_name=None, top=1000, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_module_list( + cmd, device_id, hub_name=None, top=1000, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -460,8 +649,12 @@ def iot_device_module_list(cmd, device_id, hub_name=None, top=1000, resource_gro raise CLIError(unpack_msrest_error(e)) -def iot_device_module_show(cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_module_show( + cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_device_module_show(target, device_id, module_id) @@ -470,25 +663,35 @@ def _iot_device_module_show(target, device_id, module_id): service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - module = service_sdk.registry_manager.get_module(id=device_id, mid=module_id, raw=True).response.json() + module = service_sdk.registry_manager.get_module( + id=device_id, mid=module_id, raw=True + ).response.json() module["hub"] = target.get("entity") return module except CloudError as e: raise CLIError(unpack_msrest_error(e)) -def iot_device_module_delete(cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_module_delete( + cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - module = _iot_device_module_show(target=target, device_id=device_id, module_id=module_id) + module = _iot_device_module_show( + target=target, device_id=device_id, module_id=module_id + ) etag = module.get("etag") if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - service_sdk.registry_manager.delete_module(id=device_id, mid=module_id, custom_headers=headers) + service_sdk.registry_manager.delete_module( + id=device_id, mid=module_id, custom_headers=headers + ) return raise LookupError("module etag not found") except CloudError as e: @@ -497,9 +700,15 @@ def iot_device_module_delete(cmd, device_id, module_id, hub_name=None, resource_ raise CLIError(err) -def iot_device_module_twin_show(cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) - return _iot_device_module_twin_show(target=target, device_id=device_id, module_id=module_id) +def iot_device_module_twin_show( + cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) + return _iot_device_module_twin_show( + target=target, device_id=device_id, module_id=module_id + ) def _iot_device_module_twin_show(target, device_id, module_id): @@ -507,15 +716,27 @@ def _iot_device_module_twin_show(target, device_id, module_id): service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - return service_sdk.twin.get_module_twin(id=device_id, mid=module_id, raw=True).response.json() + return service_sdk.twin.get_module_twin( + id=device_id, mid=module_id, raw=True + ).response.json() except CloudError as e: raise CLIError(unpack_msrest_error(e)) -def iot_device_module_twin_update(cmd, device_id, module_id, parameters, hub_name=None, resource_group_name=None, login=None): +def iot_device_module_twin_update( + cmd, + device_id, + module_id, + parameters, + hub_name=None, + resource_group_name=None, + login=None, +): from azext_iot.common.utility import verify_transform - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -524,7 +745,7 @@ def iot_device_module_twin_update(cmd, device_id, module_id, parameters, hub_nam if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - verify = {'properties.desired': dict} + verify = {"properties.desired": dict} if parameters.get("tags"): verify["tags"] = dict @@ -535,7 +756,7 @@ def iot_device_module_twin_update(cmd, device_id, module_id, parameters, hub_nam id=device_id, mid=module_id, device_twin_info=parameters, - custom_headers=headers + custom_headers=headers, ) raise LookupError("module twin etag not found") except CloudError as e: @@ -544,17 +765,25 @@ def iot_device_module_twin_update(cmd, device_id, module_id, parameters, hub_nam raise CLIError(err) -def iot_device_module_twin_replace(cmd, device_id, module_id, target_json, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_module_twin_replace( + cmd, + device_id, + module_id, + target_json, + hub_name=None, + resource_group_name=None, + login=None, +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) try: target_json = process_json_arg(target_json, argument_name="json") module = _iot_device_module_twin_show( - target=target, - device_id=device_id, - module_id=module_id + target=target, device_id=device_id, module_id=module_id ) etag = module.get("etag") if etag: @@ -564,7 +793,7 @@ def iot_device_module_twin_replace(cmd, device_id, module_id, target_json, hub_n id=device_id, mid=module_id, device_twin_info=target_json, - custom_headers=headers + custom_headers=headers, ) raise LookupError("module twin etag not found") except CloudError as e: @@ -573,17 +802,22 @@ def iot_device_module_twin_replace(cmd, device_id, module_id, target_json, hub_n raise CLIError(err) -def iot_edge_set_modules(cmd, device_id, content, - hub_name=None, resource_group_name=None, login=None): +def iot_edge_set_modules( + cmd, device_id, content, hub_name=None, resource_group_name=None, login=None +): from azext_iot.sdk.iothub.service.models import ConfigurationContent - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - content = process_json_arg(content, argument_name='content') - processed_content = _process_config_content(content, config_type=ConfigType.edge) + content = process_json_arg(content, argument_name="content") + processed_content = _process_config_content( + content, config_type=ConfigType.edge + ) content = ConfigurationContent(**processed_content) service_sdk.configuration.apply_on_edge_device(id=device_id, content=content) @@ -592,32 +826,84 @@ def iot_edge_set_modules(cmd, device_id, content, raise CLIError(unpack_msrest_error(e)) -def iot_edge_deployment_create(cmd, config_id, content, hub_name=None, target_condition="", priority=0, - labels=None, metrics=None, layered=False, resource_group_name=None, login=None): +def iot_edge_deployment_create( + cmd, + config_id, + content, + hub_name=None, + target_condition="", + priority=0, + labels=None, + metrics=None, + layered=False, + resource_group_name=None, + login=None, +): config_type = ConfigType.layered if layered else ConfigType.edge - return _iot_hub_configuration_create(cmd=cmd, config_id=config_id, content=content, hub_name=hub_name, - target_condition=target_condition, priority=priority, - labels=labels, metrics=metrics, resource_group_name=resource_group_name, - login=login, config_type=config_type) + return _iot_hub_configuration_create( + cmd=cmd, + config_id=config_id, + content=content, + hub_name=hub_name, + target_condition=target_condition, + priority=priority, + labels=labels, + metrics=metrics, + resource_group_name=resource_group_name, + login=login, + config_type=config_type, + ) -def iot_hub_configuration_create(cmd, config_id, content, hub_name=None, target_condition="", priority=0, - labels=None, metrics=None, resource_group_name=None, login=None): - return _iot_hub_configuration_create(cmd=cmd, config_id=config_id, content=content, hub_name=hub_name, - target_condition=target_condition, priority=priority, - labels=labels, metrics=metrics, resource_group_name=resource_group_name, - login=login, config_type=ConfigType.adm) +def iot_hub_configuration_create( + cmd, + config_id, + content, + hub_name=None, + target_condition="", + priority=0, + labels=None, + metrics=None, + resource_group_name=None, + login=None, +): + return _iot_hub_configuration_create( + cmd=cmd, + config_id=config_id, + content=content, + hub_name=hub_name, + target_condition=target_condition, + priority=priority, + labels=labels, + metrics=metrics, + resource_group_name=resource_group_name, + login=login, + config_type=ConfigType.adm, + ) -def _iot_hub_configuration_create(cmd, config_id, content, config_type, hub_name=None, target_condition="", priority=0, - labels=None, metrics=None, resource_group_name=None, login=None): +def _iot_hub_configuration_create( + cmd, + config_id, + content, + config_type, + hub_name=None, + target_condition="", + priority=0, + labels=None, + metrics=None, + resource_group_name=None, + login=None, +): from azext_iot.sdk.service.models import ( Configuration, ConfigurationContent, - ConfigurationMetrics + ConfigurationMetrics, ) - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -631,7 +917,11 @@ def _iot_hub_configuration_create(cmd, config_id, content, config_type, hub_name required_target_prefix = "from devices.modules where" lower_target_condition = target_condition.lower() if not lower_target_condition.startswith(required_target_prefix): - raise CLIError("The target condition for a module configuration must start with '{}'".format(required_target_prefix)) + raise CLIError( + "The target condition for a module configuration must start with '{}'".format( + required_target_prefix + ) + ) if metrics: metrics = process_json_arg(metrics, argument_name="metrics") @@ -639,7 +929,9 @@ def _iot_hub_configuration_create(cmd, config_id, content, config_type, hub_name if "metrics" in metrics: metrics = metrics["metrics"] if metrics_key not in metrics: - raise CLIError("metrics json must include the '{}' property".format(metrics_key)) + raise CLIError( + "metrics json must include the '{}' property".format(metrics_key) + ) metrics = metrics[metrics_key] if labels: @@ -648,17 +940,21 @@ def _iot_hub_configuration_create(cmd, config_id, content, config_type, hub_name config_content = ConfigurationContent(**processed_content) config_metrics = ConfigurationMetrics(queries=metrics) - config = Configuration(id=config_id, - schema_version="2.0", - labels=labels, - content=config_content, - metrics=config_metrics, - target_condition=target_condition, - etag="*", - priority=priority, - content_type="assignment") + config = Configuration( + id=config_id, + schema_version="2.0", + labels=labels, + content=config_content, + metrics=config_metrics, + target_condition=target_condition, + etag="*", + priority=priority, + content_type="assignment", + ) try: - return service_sdk.configuration.create_or_update(id=config_id, configuration=config) + return service_sdk.configuration.create_or_update( + id=config_id, configuration=config + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -680,9 +976,11 @@ def _process_config_content(content, config_type): processed_content[to_snake_case(key)] = content[key] return processed_content - raise CLIError("Automatic device configuration payloads require property: {}".format( - ' or '.join(map(str, valid_adm_keys)) - )) + raise CLIError( + "Automatic device configuration payloads require property: {}".format( + " or ".join(map(str, valid_adm_keys)) + ) + ) if config_type == ConfigType.edge or config_type == ConfigType.layered: valid_edge_key = "modulesContent" @@ -703,12 +1001,16 @@ def _process_config_content(content, config_type): if config_type == ConfigType.edge: _validate_payload_schema(processed_content) - processed_content[to_snake_case(valid_edge_key)] = processed_content[valid_edge_key] + processed_content[to_snake_case(valid_edge_key)] = processed_content[ + valid_edge_key + ] del processed_content[valid_edge_key] return processed_content - raise CLIError("Edge deployment payloads require property: {}".format(valid_edge_key)) + raise CLIError( + "Edge deployment payloads require property: {}".format(valid_edge_key) + ) def _validate_payload_schema(content): @@ -733,16 +1035,22 @@ def _validate_payload_schema(content): errors = v.validate(content) if errors: # Pretty printing schema validation errors - raise CLIError(json.dumps({"validationErrors": errors}, separators=(",", ":"), indent=2)) + raise CLIError( + json.dumps({"validationErrors": errors}, separators=(",", ":"), indent=2) + ) return -def iot_hub_configuration_update(cmd, config_id, parameters, hub_name=None, resource_group_name=None, login=None): +def iot_hub_configuration_update( + cmd, config_id, parameters, hub_name=None, resource_group_name=None, login=None +): from azext_iot.sdk.service.models.configuration import Configuration from azext_iot.common.utility import verify_transform - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -752,27 +1060,35 @@ def iot_hub_configuration_update(cmd, config_id, parameters, hub_name=None, reso raise LookupError("invalid request, configuration etag not found") headers = {} headers["If-Match"] = '"{}"'.format(etag) - verify = {'metrics': dict, 'metrics.queries': dict, 'content': dict} + verify = {"metrics": dict, "metrics.queries": dict, "content": dict} if parameters.get("labels"): verify["labels"] = dict verify_transform(parameters, verify) - config = Configuration(id=parameters['id'], - schema_version=parameters['schemaVersion'], - labels=parameters['labels'], - content=parameters['content'], - metrics=parameters.get('metrics', None), - target_condition=parameters['targetCondition'], - priority=parameters['priority'], - content_type='assignment') - return service_sdk.configuration.create_or_update(id=config_id, configuration=config, custom_headers=headers) + config = Configuration( + id=parameters["id"], + schema_version=parameters["schemaVersion"], + labels=parameters["labels"], + content=parameters["content"], + metrics=parameters.get("metrics", None), + target_condition=parameters["targetCondition"], + priority=parameters["priority"], + content_type="assignment", + ) + return service_sdk.configuration.create_or_update( + id=config_id, configuration=config, custom_headers=headers + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) except (AttributeError, LookupError, TypeError) as err: raise CLIError(err) -def iot_hub_configuration_show(cmd, config_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_hub_configuration_show( + cmd, config_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_hub_configuration_show(target=target, config_id=config_id) @@ -786,30 +1102,54 @@ def _iot_hub_configuration_show(target, config_id): raise CLIError(unpack_msrest_error(e)) -def iot_hub_configuration_list(cmd, hub_name=None, top=10, resource_group_name=None, login=None): - result = _iot_hub_configuration_list(cmd, hub_name=hub_name, top=top, - resource_group_name=resource_group_name, login=login) - filtered = [c for c in result if (c["content"].get("deviceContent") or c["content"].get("moduleContent"))] +def iot_hub_configuration_list( + cmd, hub_name=None, top=10, resource_group_name=None, login=None +): + result = _iot_hub_configuration_list( + cmd, + hub_name=hub_name, + top=top, + resource_group_name=resource_group_name, + login=login, + ) + filtered = [ + c + for c in result + if (c["content"].get("deviceContent") or c["content"].get("moduleContent")) + ] return filtered[:top] -def iot_edge_deployment_list(cmd, hub_name=None, top=10, resource_group_name=None, login=None): - result = _iot_hub_configuration_list(cmd, hub_name=hub_name, top=top, - resource_group_name=resource_group_name, login=login) +def iot_edge_deployment_list( + cmd, hub_name=None, top=10, resource_group_name=None, login=None +): + result = _iot_hub_configuration_list( + cmd, + hub_name=hub_name, + top=top, + resource_group_name=resource_group_name, + login=login, + ) filtered = [c for c in result if c["content"].get("modulesContent")] return filtered[:top] -def _iot_hub_configuration_list(cmd, hub_name=None, top=10, resource_group_name=None, login=None): +def _iot_hub_configuration_list( + cmd, hub_name=None, top=10, resource_group_name=None, login=None +): top = _process_top(top, upper_limit=100) - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - result = service_sdk.configuration.get_configurations(top=top, raw=True).response.json() + result = service_sdk.configuration.get_configurations( + top=top, raw=True + ).response.json() if not result: logger.info('No configurations found on hub "%s".', hub_name) return result @@ -817,8 +1157,12 @@ def _iot_hub_configuration_list(cmd, hub_name=None, top=10, resource_group_name= raise CLIError(unpack_msrest_error(e)) -def iot_hub_configuration_delete(cmd, config_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_hub_configuration_delete( + cmd, config_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -837,16 +1181,38 @@ def iot_hub_configuration_delete(cmd, config_id, hub_name=None, resource_group_n raise CLIError(err) -def iot_edge_deployment_metric_show(cmd, config_id, metric_id, metric_type='user', - hub_name=None, resource_group_name=None, login=None): - return iot_hub_configuration_metric_show(cmd, config_id=config_id, metric_id=metric_id, - metric_type=metric_type, hub_name=hub_name, - resource_group_name=resource_group_name, login=login) +def iot_edge_deployment_metric_show( + cmd, + config_id, + metric_id, + metric_type="user", + hub_name=None, + resource_group_name=None, + login=None, +): + return iot_hub_configuration_metric_show( + cmd, + config_id=config_id, + metric_id=metric_id, + metric_type=metric_type, + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + ) -def iot_hub_configuration_metric_show(cmd, config_id, metric_id, metric_type='user', - hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_hub_configuration_metric_show( + cmd, + config_id, + metric_id, + metric_type="user", + hub_name=None, + resource_group_name=None, + login=None, +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -854,13 +1220,17 @@ def iot_hub_configuration_metric_show(cmd, config_id, metric_id, metric_type='us config = _iot_hub_configuration_show(target=target, config_id=config_id) metric_collection = None - if metric_type == 'system': - metric_collection = config['systemMetrics'].get('queries') + if metric_type == "system": + metric_collection = config["systemMetrics"].get("queries") else: - metric_collection = config['metrics'].get('queries') + metric_collection = config["metrics"].get("queries") if metric_id not in metric_collection: - raise CLIError("the metric '{}' is not defined in the device configuration '{}'".format(metric_id, config_id)) + raise CLIError( + "the metric '{}' is not defined in the device configuration '{}'".format( + metric_id, config_id + ) + ) metric_query = metric_collection[metric_id] @@ -870,9 +1240,9 @@ def iot_hub_configuration_metric_show(cmd, config_id, metric_id, metric_type='us metric_result = _execute_query(query_args, query_method, None) output = {} - output['metric'] = metric_id - output['query'] = metric_query - output['result'] = metric_result + output["metric"] = metric_id + output["query"] = metric_query + output["result"] = metric_result return output except CloudError as e: @@ -881,8 +1251,13 @@ def iot_hub_configuration_metric_show(cmd, config_id, metric_id, metric_type='us # Device Twin -def iot_device_twin_show(cmd, device_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + +def iot_device_twin_show( + cmd, device_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_device_twin_show(target=target, device_id=device_id) @@ -896,10 +1271,14 @@ def _iot_device_twin_show(target, device_id): raise CLIError(unpack_msrest_error(e)) -def iot_device_twin_update(cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None): +def iot_device_twin_update( + cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None +): from azext_iot.common.utility import verify_transform - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -908,14 +1287,12 @@ def iot_device_twin_update(cmd, device_id, parameters, hub_name=None, resource_g if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - verify = {'properties.desired': dict} - if parameters.get('tags', None): - verify['tags'] = dict + verify = {"properties.desired": dict} + if parameters.get("tags", None): + verify["tags"] = dict verify_transform(parameters, verify) return service_sdk.twin.update_device_twin( - id=device_id, - device_twin_info=parameters, - custom_headers=headers + id=device_id, device_twin_info=parameters, custom_headers=headers ) raise LookupError("device twin etag not found") except CloudError as e: @@ -924,8 +1301,12 @@ def iot_device_twin_update(cmd, device_id, parameters, hub_name=None, resource_g raise CLIError(err) -def iot_device_twin_replace(cmd, device_id, target_json, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_twin_replace( + cmd, device_id, target_json, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -937,9 +1318,7 @@ def iot_device_twin_replace(cmd, device_id, target_json, hub_name=None, resource headers = {} headers["If-Match"] = '"{}"'.format(etag) return service_sdk.twin.replace_device_twin( - id=device_id, - device_twin_info=target_json, - custom_headers=headers + id=device_id, device_twin_info=target_json, custom_headers=headers ) raise LookupError("device twin etag not found") except CloudError as e: @@ -948,17 +1327,34 @@ def iot_device_twin_replace(cmd, device_id, target_json, hub_name=None, resource raise CLIError(err) -def iot_device_method(cmd, device_id, method_name, hub_name=None, method_payload="{}", - timeout=30, resource_group_name=None, login=None): +def iot_device_method( + cmd, + device_id, + method_name, + hub_name=None, + method_payload="{}", + timeout=30, + resource_group_name=None, + login=None, +): from azext_iot.sdk.service.models import CloudToDeviceMethod - from azext_iot.constants import METHOD_INVOKE_MAX_TIMEOUT_SEC, METHOD_INVOKE_MIN_TIMEOUT_SEC + from azext_iot.constants import ( + METHOD_INVOKE_MAX_TIMEOUT_SEC, + METHOD_INVOKE_MIN_TIMEOUT_SEC, + ) if timeout > METHOD_INVOKE_MAX_TIMEOUT_SEC: - raise CLIError('timeout must not be over {} seconds'.format(METHOD_INVOKE_MAX_TIMEOUT_SEC)) + raise CLIError( + "timeout must not be over {} seconds".format(METHOD_INVOKE_MAX_TIMEOUT_SEC) + ) if timeout < METHOD_INVOKE_MIN_TIMEOUT_SEC: - raise CLIError('timeout must be at least {} seconds'.format(METHOD_INVOKE_MIN_TIMEOUT_SEC)) + raise CLIError( + "timeout must be at least {} seconds".format(METHOD_INVOKE_MIN_TIMEOUT_SEC) + ) - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -966,13 +1362,13 @@ def iot_device_method(cmd, device_id, method_name, hub_name=None, method_payload service_sdk.config.retry_policy.retries = 1 try: if method_payload: - method_payload = process_json_arg(method_payload, argument_name="method-payload") + method_payload = process_json_arg( + method_payload, argument_name="method-payload" + ) method = CloudToDeviceMethod(method_name, timeout, timeout, method_payload) return service_sdk.device_method.invoke_device_method( - device_id=device_id, - direct_method_request=method, - timeout=timeout + device_id=device_id, direct_method_request=method, timeout=timeout ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -980,17 +1376,36 @@ def iot_device_method(cmd, device_id, method_name, hub_name=None, method_payload # Device Module Method Invoke -def iot_device_module_method(cmd, device_id, module_id, method_name, hub_name=None, method_payload="{}", - timeout=30, resource_group_name=None, login=None): + +def iot_device_module_method( + cmd, + device_id, + module_id, + method_name, + hub_name=None, + method_payload="{}", + timeout=30, + resource_group_name=None, + login=None, +): from azext_iot.sdk.service.models.cloud_to_device_method import CloudToDeviceMethod - from azext_iot.constants import METHOD_INVOKE_MAX_TIMEOUT_SEC, METHOD_INVOKE_MIN_TIMEOUT_SEC + from azext_iot.constants import ( + METHOD_INVOKE_MAX_TIMEOUT_SEC, + METHOD_INVOKE_MIN_TIMEOUT_SEC, + ) if timeout > METHOD_INVOKE_MAX_TIMEOUT_SEC: - raise CLIError('timeout must not be over {} seconds'.format(METHOD_INVOKE_MAX_TIMEOUT_SEC)) + raise CLIError( + "timeout must not be over {} seconds".format(METHOD_INVOKE_MAX_TIMEOUT_SEC) + ) if timeout < METHOD_INVOKE_MIN_TIMEOUT_SEC: - raise CLIError('timeout must not be over {} seconds'.format(METHOD_INVOKE_MIN_TIMEOUT_SEC)) + raise CLIError( + "timeout must not be over {} seconds".format(METHOD_INVOKE_MIN_TIMEOUT_SEC) + ) - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -998,14 +1413,16 @@ def iot_device_module_method(cmd, device_id, module_id, method_name, hub_name=No service_sdk.config.retry_policy.retries = 1 try: if method_payload: - method_payload = process_json_arg(method_payload, argument_name="method-payload") + method_payload = process_json_arg( + method_payload, argument_name="method-payload" + ) method = CloudToDeviceMethod(method_name, timeout, timeout, method_payload) return service_sdk.device_method.invoke_module_method( device_id=device_id, module_id=module_id, direct_method_request=method, - timeout=timeout + timeout=timeout, ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -1013,115 +1430,217 @@ def iot_device_module_method(cmd, device_id, module_id, method_name, hub_name=No # Utility -def iot_get_sas_token(cmd, hub_name=None, device_id=None, policy_name='iothubowner', key_type='primary', - duration=3600, resource_group_name=None, login=None, module_id=None): + +def iot_get_sas_token( + cmd, + hub_name=None, + device_id=None, + policy_name="iothubowner", + key_type="primary", + duration=3600, + resource_group_name=None, + login=None, + module_id=None, +): key_type = key_type.lower() policy_name = policy_name.lower() - if login and policy_name != 'iothubowner': - raise CLIError('You are unable to change the sas policy with a hub connection string login.') - if login and key_type != 'primary' and not device_id: - raise CLIError('For non-device sas, you are unable to change the key type with a connection string login.') + if login and policy_name != "iothubowner": + raise CLIError( + "You are unable to change the sas policy with a hub connection string login." + ) + if login and key_type != "primary" and not device_id: + raise CLIError( + "For non-device sas, you are unable to change the key type with a connection string login." + ) if module_id and not device_id: - raise CLIError('You are unable to get sas token for module without device information.') + raise CLIError( + "You are unable to get sas token for module without device information." + ) - return {'sas': _iot_build_sas_token(cmd, hub_name, device_id, module_id, - policy_name, key_type, duration, resource_group_name, login).generate_sas_token()} + return { + "sas": _iot_build_sas_token( + cmd, + hub_name, + device_id, + module_id, + policy_name, + key_type, + duration, + resource_group_name, + login, + ).generate_sas_token() + } -def _iot_build_sas_token(cmd, hub_name=None, device_id=None, module_id=None, policy_name='iothubowner', - key_type='primary', duration=3600, resource_group_name=None, login=None): - from azext_iot.common._azure import (parse_iot_device_connection_string, - parse_iot_device_module_connection_string) +def _iot_build_sas_token( + cmd, + hub_name=None, + device_id=None, + module_id=None, + policy_name="iothubowner", + key_type="primary", + duration=3600, + resource_group_name=None, + login=None, +): + from azext_iot.common._azure import ( + parse_iot_device_connection_string, + parse_iot_device_module_connection_string, + ) - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, policy_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, policy_name, login=login + ) uri = None policy = None key = None if device_id: - logger.info('Obtaining device "%s" details from registry, using IoT Hub policy "%s"', device_id, policy_name) + logger.info( + 'Obtaining device "%s" details from registry, using IoT Hub policy "%s"', + device_id, + policy_name, + ) device = _iot_device_show(target, device_id) if module_id: module = _iot_device_module_show(target, device_id, module_id) - module_cs = _build_device_or_module_connection_string(entity=module, key_type=key_type) - uri = '{}/devices/{}/modules/{}'.format(target['entity'], device_id, module_id) + module_cs = _build_device_or_module_connection_string( + entity=module, key_type=key_type + ) + uri = "{}/devices/{}/modules/{}".format( + target["entity"], device_id, module_id + ) try: parsed_module_cs = parse_iot_device_module_connection_string(module_cs) except ValueError as e: logger.debug(e) - raise CLIError('This module does not support SAS auth.') + raise CLIError("This module does not support SAS auth.") - key = parsed_module_cs['SharedAccessKey'] + key = parsed_module_cs["SharedAccessKey"] else: - device_cs = _build_device_or_module_connection_string(entity=device, key_type=key_type) - uri = '{}/devices/{}'.format(target['entity'], device_id) + device_cs = _build_device_or_module_connection_string( + entity=device, key_type=key_type + ) + uri = "{}/devices/{}".format(target["entity"], device_id) try: parsed_device_cs = parse_iot_device_connection_string(device_cs) except ValueError as e: logger.debug(e) - raise CLIError('This device does not support SAS auth.') + raise CLIError("This device does not support SAS auth.") - key = parsed_device_cs['SharedAccessKey'] + key = parsed_device_cs["SharedAccessKey"] else: - uri = target['entity'] - policy = target['policy'] - key = target['primarykey'] if key_type == 'primary' else target['secondarykey'] + uri = target["entity"] + policy = target["policy"] + key = target["primarykey"] if key_type == "primary" else target["secondarykey"] return SasTokenAuthentication(uri, policy, key, duration) -def _build_device_or_module_connection_string(entity, key_type='primary'): - is_device = entity.get('moduleId') is None - template = 'HostName={};DeviceId={};{}' if is_device else 'HostName={};DeviceId={};ModuleId={};{}' - auth = entity['authentication'] +def _build_device_or_module_connection_string(entity, key_type="primary"): + is_device = entity.get("moduleId") is None + template = ( + "HostName={};DeviceId={};{}" + if is_device + else "HostName={};DeviceId={};ModuleId={};{}" + ) + auth = entity["authentication"] auth_type = auth["type"].lower() if auth_type == "sas": - key = 'SharedAccessKey={}'.format( - auth['symmetricKey']['primaryKey'] if key_type == 'primary' else auth['symmetricKey']['secondaryKey']) - elif auth_type in ['certificateauthority', 'selfsigned']: + key = "SharedAccessKey={}".format( + auth["symmetricKey"]["primaryKey"] + if key_type == "primary" + else auth["symmetricKey"]["secondaryKey"] + ) + elif auth_type in ["certificateauthority", "selfsigned"]: key = "x509=true" else: - raise CLIError('Unable to form target connection string') + raise CLIError("Unable to form target connection string") if is_device: - return template.format(entity.get('hub'), entity.get('deviceId'), key) + return template.format(entity.get("hub"), entity.get("deviceId"), key) else: - return template.format(entity.get('hub'), entity.get('deviceId'), entity.get('moduleId'), key) + return template.format( + entity.get("hub"), entity.get("deviceId"), entity.get("moduleId"), key + ) -def iot_get_device_connection_string(cmd, device_id, hub_name=None, key_type='primary', - resource_group_name=None, login=None): +def iot_get_device_connection_string( + cmd, + device_id, + hub_name=None, + key_type="primary", + resource_group_name=None, + login=None, +): result = {} - device = iot_device_show(cmd, device_id, - hub_name=hub_name, resource_group_name=resource_group_name, login=login) - result['connectionString'] = _build_device_or_module_connection_string(device, key_type) + device = iot_device_show( + cmd, + device_id, + hub_name=hub_name, + resource_group_name=resource_group_name, + login=login, + ) + result["connectionString"] = _build_device_or_module_connection_string( + device, key_type + ) return result -def iot_get_module_connection_string(cmd, device_id, module_id, hub_name=None, key_type='primary', - resource_group_name=None, login=None): +def iot_get_module_connection_string( + cmd, + device_id, + module_id, + hub_name=None, + key_type="primary", + resource_group_name=None, + login=None, +): result = {} - module = iot_device_module_show(cmd, device_id, module_id, - resource_group_name=resource_group_name, hub_name=hub_name, login=login) - result['connectionString'] = _build_device_or_module_connection_string(module, key_type) + module = iot_device_module_show( + cmd, + device_id, + module_id, + resource_group_name=resource_group_name, + hub_name=hub_name, + login=login, + ) + result["connectionString"] = _build_device_or_module_connection_string( + module, key_type + ) return result # Messaging -def iot_device_send_message(cmd, device_id, hub_name=None, data='Ping from Az CLI IoT Extension', - properties=None, msg_count=1, resource_group_name=None, login=None, qos=1): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + +def iot_device_send_message( + cmd, + device_id, + hub_name=None, + data="Ping from Az CLI IoT Extension", + properties=None, + msg_count=1, + resource_group_name=None, + login=None, + qos=1, +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_device_send_message( target=target, device_id=device_id, data=data, properties=properties, msg_count=msg_count, - qos=qos) + qos=qos, + ) -def _iot_device_send_message(target, device_id, data, properties=None, msg_count=1, qos=1): +def _iot_device_send_message( + target, device_id, data, properties=None, msg_count=1, qos=1 +): from azext_iot.operations._mqtt import build_mqtt_device_username import paho.mqtt.publish as publish from paho.mqtt import client as mqtt @@ -1132,29 +1651,49 @@ def _iot_device_send_message(target, device_id, data, properties=None, msg_count if properties: properties = validate_key_value_pairs(properties) - sas = SasTokenAuthentication(target['entity'], target['policy'], target['primarykey'], 360).generate_sas_token() + sas = SasTokenAuthentication( + target["entity"], target["policy"], target["primarykey"], 360 + ).generate_sas_token() cwd = EXTENSION_ROOT - cert_path = os.path.join(cwd, 'digicert.pem') + cert_path = os.path.join(cwd, "digicert.pem") auth = { "username": build_mqtt_device_username(target["entity"], device_id), "password": sas, } - tls = {'ca_certs': cert_path, 'tls_version': ssl.PROTOCOL_SSLv23} - topic = 'devices/{}/messages/events/{}'.format(device_id, url_encode_dict(properties) if properties else '') + tls = {"ca_certs": cert_path, "tls_version": ssl.PROTOCOL_SSLv23} + topic = "devices/{}/messages/events/{}".format( + device_id, url_encode_dict(properties) if properties else "" + ) for _ in range(msg_count): - msgs.append({'topic': topic, 'payload': data, 'qos': int(qos)}) + msgs.append({"topic": topic, "payload": data, "qos": int(qos)}) try: - publish.multiple(msgs, client_id=device_id, hostname=target['entity'], - auth=auth, port=8883, protocol=mqtt.MQTTv311, tls=tls) + publish.multiple( + msgs, + client_id=device_id, + hostname=target["entity"], + auth=auth, + port=8883, + protocol=mqtt.MQTTv311, + tls=tls, + ) return except Exception as x: raise CLIError(x) -def iot_device_send_message_http(cmd, device_id, data, hub_name=None, headers=None, - resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_device_send_message_http( + cmd, + device_id, + data, + hub_name=None, + headers=None, + resource_group_name=None, + login=None, +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_device_send_message_http(target, device_id, data, headers) @@ -1163,13 +1702,19 @@ def _iot_device_send_message_http(target, device_id, data, headers=None): device_sdk = resolver.get_sdk(SdkType.device_sdk) try: - return device_sdk.device.send_device_event(id=device_id, message=data, custom_headers=headers) + return device_sdk.device.send_device_event( + id=device_id, message=data, custom_headers=headers + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) -def iot_c2d_message_complete(cmd, device_id, etag, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_c2d_message_complete( + cmd, device_id, etag, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_c2d_message_complete(target, device_id, etag) @@ -1178,13 +1723,19 @@ def _iot_c2d_message_complete(target, device_id, etag): device_sdk = resolver.get_sdk(SdkType.device_sdk) try: - return device_sdk.device.complete_device_bound_notification(id=device_id, etag=etag) + return device_sdk.device.complete_device_bound_notification( + id=device_id, etag=etag + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) -def iot_c2d_message_reject(cmd, device_id, etag, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_c2d_message_reject( + cmd, device_id, etag, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_c2d_message_reject(target, device_id, etag) @@ -1193,13 +1744,19 @@ def _iot_c2d_message_reject(target, device_id, etag): device_sdk = resolver.get_sdk(SdkType.device_sdk) try: - return device_sdk.device.complete_device_bound_notification(id=device_id, etag=etag, reject='') + return device_sdk.device.complete_device_bound_notification( + id=device_id, etag=etag, reject="" + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) -def iot_c2d_message_abandon(cmd, device_id, etag, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_c2d_message_abandon( + cmd, device_id, etag, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_c2d_message_abandon(target, device_id, etag) @@ -1208,13 +1765,19 @@ def _iot_c2d_message_abandon(target, device_id, etag): device_sdk = resolver.get_sdk(SdkType.device_sdk) try: - return device_sdk.device.abandon_device_bound_notification(id=device_id, etag=etag) + return device_sdk.device.abandon_device_bound_notification( + id=device_id, etag=etag + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) -def iot_c2d_message_receive(cmd, device_id, hub_name=None, lock_timeout=60, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_c2d_message_receive( + cmd, device_id, hub_name=None, lock_timeout=60, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) return _iot_c2d_message_receive(target, device_id, lock_timeout) @@ -1226,13 +1789,12 @@ def _iot_c2d_message_receive(target, device_id, lock_timeout=60): request_headers = {} if lock_timeout: - request_headers['IotHub-MessageLockTimeout'] = str(lock_timeout) + request_headers["IotHub-MessageLockTimeout"] = str(lock_timeout) try: result = device_sdk.device.receive_device_bound_notification( - id=device_id, - custom_headers=request_headers, - raw=True).response + id=device_id, custom_headers=request_headers, raw=True + ).response if result and result.status_code == 200: payload = {"properties": {}} @@ -1240,12 +1802,16 @@ def _iot_c2d_message_receive(target, device_id, lock_timeout=60): if "etag" in result.headers: payload["etag"] = result.headers["etag"].strip('"') - app_prop_prefix = 'iothub-app-' - app_prop_keys = [header for header in result.headers if header.lower().startswith(app_prop_prefix)] + app_prop_prefix = "iothub-app-" + app_prop_keys = [ + header + for header in result.headers + if header.lower().startswith(app_prop_prefix) + ] app_props = {} for key in app_prop_keys: - app_props[key[len(app_prop_prefix):]] = result.headers[key] + app_props[key[len(app_prop_prefix) :]] = result.headers[key] if app_props: payload["properties"]["app"] = app_props @@ -1259,7 +1825,11 @@ def _iot_c2d_message_receive(target, device_id, lock_timeout=60): payload["properties"]["system"] = sys_props if result.text: - payload["data"] = result.text if not isinstance(result.text, six.binary_type) else result.text.decode("utf-8") + payload["data"] = ( + result.text + if not isinstance(result.text, six.binary_type) + else result.text.decode("utf-8") + ) return payload return @@ -1267,23 +1837,41 @@ def _iot_c2d_message_receive(target, device_id, lock_timeout=60): raise CLIError(unpack_msrest_error(e)) -def iot_c2d_message_send(cmd, device_id, hub_name=None, data='Ping from Az CLI IoT Extension', - message_id=None, correlation_id=None, user_id=None, content_encoding="utf-8", - content_type=None, expiry_time_utc=None, properties=None, ack=None, - wait_on_feedback=False, yes=False, repair=False, resource_group_name=None, login=None): - import importlib +def iot_c2d_message_send( + cmd, + device_id, + hub_name=None, + data="Ping from Az CLI IoT Extension", + message_id=None, + correlation_id=None, + user_id=None, + content_encoding="utf-8", + content_type=None, + expiry_time_utc=None, + properties=None, + ack=None, + wait_on_feedback=False, + yes=False, + repair=False, + resource_group_name=None, + login=None, +): from azext_iot.common.deps import ensure_uamqp from azext_iot.common.utility import validate_min_python_version validate_min_python_version(3, 4) if wait_on_feedback and not ack: - raise CLIError('To wait on device feedback, ack must be "full", "negative" or "positive"') + raise CLIError( + 'To wait on device feedback, ack must be "full", "negative" or "positive"' + ) config = cmd.cli_ctx.config ensure_uamqp(config, yes, repair) - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) if properties: properties = validate_key_value_pairs(properties) @@ -1294,48 +1882,71 @@ def iot_c2d_message_send(cmd, device_id, hub_name=None, data='Ping from Az CLI I if user_msg_expiry < now_in_milli: raise CLIError("Message expiry time utc is in the past!") - events3 = importlib.import_module('azext_iot.operations.events3._events') - - msg_id, errors = events3.send_c2d_message(target=target, device_id=device_id, data=data, - message_id=message_id, correlation_id=correlation_id, - user_id=user_id, content_encoding=content_encoding, - content_type=content_type, expiry_time_utc=expiry_time_utc, - properties=properties, ack=ack) + msg_id, errors = events3.send_c2d_message( + target=target, + device_id=device_id, + data=data, + message_id=message_id, + correlation_id=correlation_id, + user_id=user_id, + content_encoding=content_encoding, + content_type=content_type, + expiry_time_utc=expiry_time_utc, + properties=properties, + ack=ack, + ) if errors: - raise CLIError('C2D message error: {}, use --debug for more details.'.format(errors)) + raise CLIError( + "C2D message error: {}, use --debug for more details.".format(errors) + ) if wait_on_feedback: _iot_hub_monitor_feedback(target=target, device_id=device_id, wait_on_id=msg_id) -def iot_simulate_device(cmd, device_id, hub_name=None, receive_settle='complete', - data='Ping from Az CLI IoT Extension', msg_count=100, - msg_interval=3, protocol_type='mqtt', properties=None, - resource_group_name=None, login=None): +def iot_simulate_device( + cmd, + device_id, + hub_name=None, + receive_settle="complete", + data="Ping from Az CLI IoT Extension", + msg_count=100, + msg_interval=3, + protocol_type="mqtt", + properties=None, + resource_group_name=None, + login=None, +): import sys import uuid import datetime import json from azext_iot.operations._mqtt import mqtt_client_wrap from azext_iot.common.utility import execute_onthread - from azext_iot.constants import MIN_SIM_MSG_INTERVAL, MIN_SIM_MSG_COUNT, SIM_RECEIVE_SLEEP_SEC + from azext_iot.constants import ( + MIN_SIM_MSG_INTERVAL, + MIN_SIM_MSG_COUNT, + SIM_RECEIVE_SLEEP_SEC, + ) protocol_type = protocol_type.lower() if protocol_type == ProtocolType.mqtt.name: - if receive_settle != 'complete': + if receive_settle != "complete": raise CLIError('mqtt protocol only supports settle type of "complete"') if msg_interval < MIN_SIM_MSG_INTERVAL: - raise CLIError('msg interval must be at least {}'.format(MIN_SIM_MSG_INTERVAL)) + raise CLIError("msg interval must be at least {}".format(MIN_SIM_MSG_INTERVAL)) if msg_count < MIN_SIM_MSG_COUNT: - raise CLIError('msg count must be at least {}'.format(MIN_SIM_MSG_COUNT)) + raise CLIError("msg count must be at least {}".format(MIN_SIM_MSG_COUNT)) properties_to_send = _iot_simulate_get_default_properties(protocol_type) user_properties = validate_key_value_pairs(properties) or {} properties_to_send.update(user_properties) - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) token = None class generator(object): @@ -1344,14 +1955,17 @@ def __init__(self): def generate(self, jsonify=True): self.calls += 1 - payload = {'id': str(uuid.uuid4()), 'timestamp': str(datetime.datetime.utcnow()), - 'data': str(data + ' #{}'.format(self.calls))} + payload = { + "id": str(uuid.uuid4()), + "timestamp": str(datetime.datetime.utcnow()), + "data": str(data + " #{}".format(self.calls)), + } return json.dumps(payload) if jsonify else payload def http_wrap(target, device_id, generator): d = generator.generate(False) _iot_device_send_message_http(target, device_id, d, headers=properties_to_send) - six.print_('.', end='', flush=True) + six.print_(".", end="", flush=True) try: if protocol_type == ProtocolType.mqtt.name: @@ -1359,15 +1973,19 @@ def http_wrap(target, device_id, generator): target=target, device_id=device_id, properties=properties_to_send, - sas_duration=(msg_count * msg_interval) + 60 # int type is enforced for msg_count and msg_interval + sas_duration=(msg_count * msg_interval) + + 60, # int type is enforced for msg_count and msg_interval ) wrap.execute(generator(), publish_delay=msg_interval, msg_count=msg_count) else: - six.print_('Sending and receiving events via https') - token, op = execute_onthread(method=http_wrap, - args=[target, device_id, generator()], - interval=msg_interval, max_runs=msg_count, - return_handle=True) + six.print_("Sending and receiving events via https") + token, op = execute_onthread( + method=http_wrap, + args=[target, device_id, generator()], + interval=msg_interval, + max_runs=msg_count, + return_handle=True, + ) while True and op.is_alive(): _handle_c2d_msg(target, device_id, receive_settle) sleep(SIM_RECEIVE_SLEEP_SEC) @@ -1395,40 +2013,66 @@ def _handle_c2d_msg(target, device_id, receive_settle): result = _iot_c2d_message_receive(target, device_id) if result: six.print_() - six.print_('__Received C2D Message__') + six.print_("__Received C2D Message__") six.print_(result) - if receive_settle == 'reject': - six.print_('__Rejecting message__') - _iot_c2d_message_reject(target, device_id, result['etag']) - elif receive_settle == 'abandon': - six.print_('__Abandoning message__') - _iot_c2d_message_abandon(target, device_id, result['etag']) + if receive_settle == "reject": + six.print_("__Rejecting message__") + _iot_c2d_message_reject(target, device_id, result["etag"]) + elif receive_settle == "abandon": + six.print_("__Abandoning message__") + _iot_c2d_message_abandon(target, device_id, result["etag"]) else: - six.print_('__Completing message__') - _iot_c2d_message_complete(target, device_id, result['etag']) + six.print_("__Completing message__") + _iot_c2d_message_complete(target, device_id, result["etag"]) return True return False -def iot_device_export(cmd, hub_name, blob_container_uri, include_keys=False, resource_group_name=None): +def iot_device_export( + cmd, hub_name, blob_container_uri, include_keys=False, resource_group_name=None +): from azext_iot._factory import iot_hub_service_factory + client = iot_hub_service_factory(cmd.cli_ctx) target = get_iot_hub_connection_string(client, hub_name, resource_group_name) - return client.export_devices(target['resourcegroup'], hub_name, blob_container_uri, not include_keys) + return client.export_devices( + target["resourcegroup"], hub_name, blob_container_uri, not include_keys + ) -def iot_device_import(cmd, hub_name, input_blob_container_uri, output_blob_container_uri, resource_group_name=None): +def iot_device_import( + cmd, + hub_name, + input_blob_container_uri, + output_blob_container_uri, + resource_group_name=None, +): from azext_iot._factory import iot_hub_service_factory + client = iot_hub_service_factory(cmd.cli_ctx) target = get_iot_hub_connection_string(client, hub_name, resource_group_name) - return client.import_devices(target['resourcegroup'], hub_name, - input_blob_container_uri, output_blob_container_uri) + return client.import_devices( + target["resourcegroup"], + hub_name, + input_blob_container_uri, + output_blob_container_uri, + ) -def iot_device_upload_file(cmd, device_id, file_path, content_type, hub_name=None, resource_group_name=None, login=None): +def iot_device_upload_file( + cmd, + device_id, + file_path, + content_type, + hub_name=None, + resource_group_name=None, + login=None, +): from azext_iot.sdk.iothub.device.models import FileUploadCompletionStatus - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) resolver = SdkResolver(target=target, device_id=device_id) device_sdk = resolver.get_sdk(SdkType.device_sdk) @@ -1441,50 +2085,77 @@ def iot_device_upload_file(cmd, device_id, file_path, content_type, hub_name=Non try: upload_meta = device_sdk.device.create_file_upload_sas_uri( - device_id=device_id, - blob_name=file_name, - raw=True + device_id=device_id, blob_name=file_name, raw=True ).response.json() storage_endpoint = "{}/{}/{}{}".format( upload_meta["hostName"], upload_meta["containerName"], upload_meta["blobName"], - upload_meta["sasToken"] + upload_meta["sasToken"], ) completion_status = FileUploadCompletionStatus( - correlation_id=upload_meta["correlationId"], - is_success=True + correlation_id=upload_meta["correlationId"], is_success=True ) upload_response = device_sdk.device.upload_file_to_container( storage_endpoint=storage_endpoint, content=content, - content_type=content_type + content_type=content_type, ) completion_status.status_code = upload_response.status_code completion_status.status_reason = upload_response.reason return device_sdk.device.update_file_upload_status( - device_id=device_id, - file_upload_completion_status=completion_status + device_id=device_id, file_upload_completion_status=completion_status ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) -def iot_hub_monitor_events(cmd, hub_name=None, device_id=None, consumer_group='$Default', timeout=300, - enqueued_time=None, resource_group_name=None, yes=False, properties=None, repair=False, - login=None, content_type=None, device_query=None): +def iot_hub_monitor_events( + cmd, + hub_name=None, + device_id=None, + consumer_group="$Default", + timeout=300, + enqueued_time=None, + resource_group_name=None, + yes=False, + properties=None, + repair=False, + login=None, + content_type=None, + device_query=None, +): try: - _iot_hub_monitor_events(cmd, hub_name=hub_name, device_id=device_id, - consumer_group=consumer_group, timeout=timeout, enqueued_time=enqueued_time, - resource_group_name=resource_group_name, yes=yes, properties=properties, - repair=repair, login=login, content_type=content_type, device_query=device_query) + _iot_hub_monitor_events( + cmd, + hub_name=hub_name, + device_id=device_id, + consumer_group=consumer_group, + timeout=timeout, + enqueued_time=enqueued_time, + resource_group_name=resource_group_name, + yes=yes, + properties=properties, + repair=repair, + login=login, + content_type=content_type, + device_query=device_query, + ) except RuntimeError as e: raise CLIError(e) -def iot_hub_monitor_feedback(cmd, hub_name=None, device_id=None, yes=False, - wait_on_id=None, repair=False, resource_group_name=None, login=None): +def iot_hub_monitor_feedback( + cmd, + hub_name=None, + device_id=None, + yes=False, + wait_on_id=None, + repair=False, + resource_group_name=None, + login=None, +): from azext_iot.common.deps import ensure_uamqp from azext_iot.common.utility import validate_min_python_version @@ -1493,74 +2164,114 @@ def iot_hub_monitor_feedback(cmd, hub_name=None, device_id=None, yes=False, config = cmd.cli_ctx.config ensure_uamqp(config, yes, repair) - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) - - return _iot_hub_monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id) - + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) -def iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name=None): - device_twin = _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name) - return _customize_device_tracing_output(device_twin['deviceId'], device_twin['properties']['desired'], - device_twin['properties']['reported']) + return _iot_hub_monitor_feedback( + target=target, device_id=device_id, wait_on_id=wait_on_id + ) -def _iot_hub_monitor_events(cmd, interface_name=None, pnp_context=None, - hub_name=None, device_id=None, consumer_group='$Default', timeout=300, - enqueued_time=None, resource_group_name=None, yes=False, properties=None, repair=False, - login=None, content_type=None, device_query=None): - import importlib +def iot_hub_distributed_tracing_show( + cmd, hub_name, device_id, resource_group_name=None +): + device_twin = _iot_hub_distributed_tracing_show( + cmd, hub_name, device_id, resource_group_name + ) + return _customize_device_tracing_output( + device_twin["deviceId"], + device_twin["properties"]["desired"], + device_twin["properties"]["reported"], + ) - (enqueued_time, properties, timeout, output) = init_monitoring(cmd, timeout, properties, enqueued_time, repair, yes) - events3 = importlib.import_module('azext_iot.operations.events3._events') - builders = importlib.import_module('azext_iot.operations.events3._builders') +def _iot_hub_monitor_events( + cmd, + interface_name=None, + pnp_context=None, + hub_name=None, + device_id=None, + consumer_group="$Default", + timeout=300, + enqueued_time=None, + resource_group_name=None, + yes=False, + properties=None, + repair=False, + login=None, + content_type=None, + device_query=None, +): + (enqueued_time, properties, timeout, output) = init_monitoring( + cmd, timeout, properties, enqueued_time, repair, yes + ) device_ids = {} if device_query: - devices_result = iot_query(cmd, device_query, hub_name, None, resource_group_name, login=login) + devices_result = iot_query( + cmd, device_query, hub_name, None, resource_group_name, login=login + ) if devices_result: for device_result in devices_result: - device_ids[device_result['deviceId']] = True + device_ids[device_result["deviceId"]] = True - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, include_events=True, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, include_events=True, login=login + ) - eventHubTarget = builders.EventTargetBuilder().build_iot_hub_target(target) + eventHubTarget = events3.EventTargetBuilder().build_iot_hub_target(target) - events3.executor(eventHubTarget, - consumer_group=consumer_group, - enqueued_time=enqueued_time, - properties=properties, - timeout=timeout, - device_id=device_id, - output=output, - content_type=content_type, - devices=device_ids, - interface_name=interface_name, - pnp_context=pnp_context) + events3.executor( + eventHubTarget, + consumer_group=consumer_group, + enqueued_time=enqueued_time, + properties=properties, + timeout=timeout, + device_id=device_id, + output=output, + content_type=content_type, + devices=device_ids, + interface_name=interface_name, + pnp_context=pnp_context, + ) -def iot_hub_distributed_tracing_update(cmd, hub_name, device_id, sampling_mode, sampling_rate, - resource_group_name=None): +def iot_hub_distributed_tracing_update( + cmd, hub_name, device_id, sampling_mode, sampling_rate, resource_group_name=None +): if int(sampling_rate) not in range(0, 101): - raise CLIError('Sampling rate is a percentage, So only values from 0 to 100(inclusive) are permitted.') - device_twin = _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name) - if TRACING_PROPERTY not in device_twin['properties']['desired']: - device_twin['properties']['desired'][TRACING_PROPERTY] = {} - device_twin['properties']['desired'][TRACING_PROPERTY]['sampling_rate'] = int(sampling_rate) - device_twin['properties']['desired'][TRACING_PROPERTY]['sampling_mode'] = 1 if sampling_mode.lower() == 'on' else 2 - result = iot_device_twin_update(cmd, device_id, device_twin, hub_name, resource_group_name) - return _customize_device_tracing_output(result.device_id, result.properties.desired, - result.properties.reported) + raise CLIError( + "Sampling rate is a percentage, So only values from 0 to 100(inclusive) are permitted." + ) + device_twin = _iot_hub_distributed_tracing_show( + cmd, hub_name, device_id, resource_group_name + ) + if TRACING_PROPERTY not in device_twin["properties"]["desired"]: + device_twin["properties"]["desired"][TRACING_PROPERTY] = {} + device_twin["properties"]["desired"][TRACING_PROPERTY]["sampling_rate"] = int( + sampling_rate + ) + device_twin["properties"]["desired"][TRACING_PROPERTY]["sampling_mode"] = ( + 1 if sampling_mode.lower() == "on" else 2 + ) + result = iot_device_twin_update( + cmd, device_id, device_twin, hub_name, resource_group_name + ) + return _customize_device_tracing_output( + result.device_id, result.properties.desired, result.properties.reported + ) def _iot_hub_monitor_feedback(target, device_id, wait_on_id): - import importlib - - events3 = importlib.import_module('azext_iot.operations.events3._events') - events3.monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id, token_duration=3600) + events3.monitor_feedback( + target=target, device_id=device_id, wait_on_id=wait_on_id, token_duration=3600 + ) -def _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name=None): +def _iot_hub_distributed_tracing_show( + cmd, hub_name, device_id, resource_group_name=None +): target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name) device_twin = iot_device_twin_show(cmd, device_id, hub_name, resource_group_name) _validate_device_tracing(target, device_twin) @@ -1568,27 +2279,41 @@ def _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_n def _validate_device_tracing(target, device_twin): - if target['location'].lower() not in TRACING_ALLOWED_FOR_LOCATION: - raise CLIError('Distributed tracing isn\'t supported for the hub located at "{}" location.' - .format(target['location'])) - if target['sku_tier'].lower() != TRACING_ALLOWED_FOR_SKU: - raise CLIError('Distributed tracing isn\'t supported for the hub belongs to "{}" sku tier.' - .format(target['sku_tier'])) - if device_twin['capabilities']['iotEdge']: - raise CLIError('The device "{}" should be non-edge device.'.format(device_twin['deviceId'])) + if target["location"].lower() not in TRACING_ALLOWED_FOR_LOCATION: + raise CLIError( + 'Distributed tracing isn\'t supported for the hub located at "{}" location.'.format( + target["location"] + ) + ) + if target["sku_tier"].lower() != TRACING_ALLOWED_FOR_SKU: + raise CLIError( + 'Distributed tracing isn\'t supported for the hub belongs to "{}" sku tier.'.format( + target["sku_tier"] + ) + ) + if device_twin["capabilities"]["iotEdge"]: + raise CLIError( + 'The device "{}" should be non-edge device.'.format(device_twin["deviceId"]) + ) def _customize_device_tracing_output(device_id, desired, reported): output = {} desired_tracing = desired.get(TRACING_PROPERTY, None) if desired_tracing: - output['deviceId'] = device_id - output['samplingMode'] = 'enabled' if desired_tracing.get('sampling_mode') == 1 else 'disabled' - output['samplingRate'] = '{}%'.format(desired_tracing.get('sampling_rate')) - output['isSynced'] = False + output["deviceId"] = device_id + output["samplingMode"] = ( + "enabled" if desired_tracing.get("sampling_mode") == 1 else "disabled" + ) + output["samplingRate"] = "{}%".format(desired_tracing.get("sampling_rate")) + output["isSynced"] = False reported_tracing = reported.get(TRACING_PROPERTY, None) - if (reported_tracing and - desired_tracing.get('sampling_mode') == reported_tracing.get('sampling_mode').get('value', None) and - desired_tracing.get('sampling_rate') == reported_tracing.get('sampling_rate').get('value', None)): - output['isSynced'] = True + if ( + reported_tracing + and desired_tracing.get("sampling_mode") + == reported_tracing.get("sampling_mode").get("value", None) + and desired_tracing.get("sampling_rate") + == reported_tracing.get("sampling_rate").get("value", None) + ): + output["isSynced"] = True return output diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 531af6d9b..64ab35e8e 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -11,41 +11,55 @@ DEVICE_ID = os.environ.get("azext_iot_central_device_id") if not all([APP_ID, DEVICE_ID]): - raise ValueError('Set azext_iot_central_app_id ' - 'and azext_iot_central_device_id to run integration tests. ') + raise ValueError( + "Set azext_iot_central_app_id " + "and azext_iot_central_device_id to run integration tests. " + ) class TestIotCentral(LiveScenarioTest): def __init__(self, test_method): - super(TestIotCentral, self).__init__('test_central_device_show') + super(TestIotCentral, self).__init__("test_central_device_show") def test_central_device_show(self): # Verify incorrect app-id throws error - self.cmd('az iotcentral device-twin show --app-id incorrect-app --device-id "{}"'. - format(DEVICE_ID), expect_failure=True) - self.cmd('az iot central device-twin show --app-id incorrect-app --device-id "{}"'. - format(DEVICE_ID), expect_failure=True) + self.cmd( + 'az iotcentral device-twin show --app-id incorrect-app --device-id "{}"'.format( + DEVICE_ID + ), + expect_failure=True, + ) # Verify incorrect device-id throws error - self.cmd('az iotcentral device-twin show --app-id "{}" --device-id incorrect-device'. - format(APP_ID), expect_failure=True) - self.cmd('az iot central device-twin show --app-id "{}" --device-id incorrect-device'. - format(APP_ID), expect_failure=True) + self.cmd( + 'az iotcentral device-twin show --app-id "{}" --device-id incorrect-device'.format( + APP_ID + ), + expect_failure=True, + ) # Verify that no errors are thrown when device shown # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices - self.cmd('az iotcentral device-twin show --app-id "{}" --device-id "{}"'. - format(APP_ID, DEVICE_ID), expect_failure=False) - self.cmd('az iot central device-twin show --app-id "{}" --device-id "{}"'. - format(APP_ID, DEVICE_ID), expect_failure=False) + self.cmd( + 'az iotcentral device-twin show --app-id "{}" --device-id "{}"'.format( + APP_ID, DEVICE_ID + ) + ) def test_central_monitor_events(self): # Test with invalid app-id - self.cmd('iotcentral app monitor-events --app-id {}'. - format(APP_ID + "zzz"), expect_failure=True) - self.cmd('iot central app monitor-events --app-id {}'. - format(APP_ID + "zzz"), expect_failure=True) + self.cmd( + "iotcentral app monitor-events --app-id {}".format(APP_ID + "zzz"), + expect_failure=True, + ) # Ensure no failure # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices - self.cmd('iotcentral app monitor-events --app-id {}'. - format(APP_ID), expect_failure=False) - self.cmd('iot central app monitor-events --app-id {}'. - format(APP_ID), expect_failure=False) + self.cmd("iotcentral app monitor-events --app-id {}".format(APP_ID)) + + def test_central_validate_messages(self): + # Test with invalid app-id + self.cmd( + "iotcentral app validate-messages --app-id {}".format(APP_ID + "zzz"), + expect_failure=True, + ) + # Ensure no failure + # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices + self.cmd("iotcentral app validate-messages --app-id {}".format(APP_ID)) diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index 3f09852cf..23b60c533 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -4,24 +4,30 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_iot.operations import central as subject -from azext_iot.common.shared import SdkType -from azure.cli.core.mock import DummyCli import mock import pytest + from knack.util import CLIError +from azext_iot.operations import central as subject +from azext_iot.common.shared import SdkType +from azure.cli.core.mock import DummyCli from azext_iot.common.utility import validate_min_python_version -device_id = 'mydevice' -app_id = 'myapp' -device_twin_result = '{device twin result}' -resource = 'shared_resource' + +device_id = "mydevice" +app_id = "myapp" +device_twin_result = "{device twin result}" +resource = "shared_resource" @pytest.fixture() def fixture_iot_token(mocker): - sas = mocker.patch('azext_iot.operations.central.get_iot_hub_token_from_central_app_id') - sas.return_value = 'SharedAccessSignature sr={}&sig=signature&se=expiry&skn=service'.format(resource) + sas = mocker.patch( + "azext_iot.operations.central.get_iot_hub_token_from_central_app_id" + ) + sas.return_value = "SharedAccessSignature sr={}&sig=signature&se=expiry&skn=service".format( + resource + ) return sas @@ -39,7 +45,7 @@ class mock_service_sdk: def get_twin(self, device_id): return device_twin_result - mock = mocker.patch('azext_iot.operations.central._bind_sdk') + mock = mocker.patch("azext_iot.operations.central._bind_sdk") mock.return_value = (mock_service_sdk(), None) return mock @@ -48,88 +54,102 @@ def get_twin(self, device_id): def fixture_requests_post(mocker): class MockJsonObject: def get(self, _value): - return '' + return "" def value(self): - return 'fixture_requests_post value' + return "fixture_requests_post value" class ReturnObject: def json(self): return MockJsonObject() - mock = mocker.patch('requests.post') + mock = mocker.patch("requests.post") mock.return_value = ReturnObject() @pytest.fixture() def fixture_azure_profile(mocker): - mock = mocker.patch('azure.cli.core._profile.Profile.__init__') + mock = mocker.patch("azure.cli.core._profile.Profile.__init__") mock.return_value = None - mock_method = mocker.patch('azure.cli.core._profile.Profile.get_raw_token') + mock_method = mocker.patch("azure.cli.core._profile.Profile.get_raw_token") class MockTokenWithGet: def get(self, _value, _default): - return 'value' + return "value" - mock_method.return_value = [['raw token 0 - A', 'raw token 0 -b', MockTokenWithGet()], 'raw token 1', 'raw token 2'] + mock_method.return_value = [ + ["raw token 0 - A", "raw token 0 -b", MockTokenWithGet()], + "raw token 1", + "raw token 2", + ] @pytest.fixture() def fixture_get_aad_token(mocker): - mock = mocker.patch('azext_iot.common._azure._get_aad_token') - mock.return_value = {'accessToken': "token"} + mock = mocker.patch("azext_iot.common._azure._get_aad_token") + mock.return_value = {"accessToken": "token"} @pytest.fixture() def fixture_get_iot_central_tokens(mocker): - mock = mocker.patch('azext_iot.common._azure.get_iot_central_tokens') + mock = mocker.patch("azext_iot.common._azure.get_iot_central_tokens") mock.return_value = { - 'eventhubSasToken': { - 'hostname': 'part1/part2/part3', - 'entityPath': 'entityPath', - 'sasToken': 'sasToken' + "eventhubSasToken": { + "hostname": "part1/part2/part3", + "entityPath": "entityPath", + "sasToken": "sasToken", }, - 'expiry': '0000', - 'iothubTenantSasToken': { - 'sasToken': 'iothubTenantSasToken' - } + "expiry": "0000", + "iothubTenantSasToken": {"sasToken": "iothubTenantSasToken"}, } -class TestCentralHelpers(): +class TestCentralHelpers: def test_get_iot_central_tokens(self, fixture_requests_post, fixture_get_aad_token): from azext_iot.common._azure import get_iot_central_tokens # Test to ensure get_iot_central_tokens calls requests.post and tokens are returned - assert get_iot_central_tokens({}, 'app_id', 'api-uri').value() == 'fixture_requests_post value' + assert ( + get_iot_central_tokens({}, "app_id", "api-uri").value() + == "fixture_requests_post value" + ) def test_get_aad_token(self, fixture_azure_profile): from azext_iot.common._azure import _get_aad_token class Cmd: - cli_ctx = 'test' + cli_ctx = "test" # Test to ensure _get_aad_token is called and returns the right values based on profile.get_raw_tokens - assert _get_aad_token(Cmd(), 'resource') == { - 'accessToken': 'raw token 0 -b', - 'expiresOn': 'value', - 'subscription': 'raw token 1', - 'tenant': 'raw token 2', - 'tokenType': 'raw token 0 - A' + assert _get_aad_token(Cmd(), "resource") == { + "accessToken": "raw token 0 -b", + "expiresOn": "value", + "subscription": "raw token 1", + "tenant": "raw token 2", + "tokenType": "raw token 0 - A", } - def test_get_iot_hub_token_from_central_app_id(self, fixture_get_iot_central_tokens): + def test_get_iot_hub_token_from_central_app_id( + self, fixture_get_iot_central_tokens + ): from azext_iot.common._azure import get_iot_hub_token_from_central_app_id # Test to ensure get_iot_hub_token_from_central_app_id returns iothubTenantSasToken - assert get_iot_hub_token_from_central_app_id({}, 'app_id', 'api-uri') == 'iothubTenantSasToken' + assert ( + get_iot_hub_token_from_central_app_id({}, "app_id", "api-uri") + == "iothubTenantSasToken" + ) -class TestDeviceTwinShow(): - def test_device_twin_show_calls_get_twin(self, fixture_iot_token, fixture_bind_sdk, fixture_cmd): - result = subject.iot_central_device_show(fixture_cmd, device_id, app_id, 'api-uri') +class TestDeviceTwinShow: + def test_device_twin_show_calls_get_twin( + self, fixture_iot_token, fixture_bind_sdk, fixture_cmd + ): + result = subject.iot_central_device_show( + fixture_cmd, device_id, app_id, "api-uri" + ) # Ensure get_twin is called and result is returned assert result is device_twin_result @@ -137,14 +157,15 @@ def test_device_twin_show_calls_get_twin(self, fixture_iot_token, fixture_bind_s # Ensure _bind_sdk is called with correct parameters assert fixture_bind_sdk.called is True args = fixture_bind_sdk.call_args - assert args[0] == ({'entity': resource}, SdkType.service_sdk) + assert args[0] == ({"entity": resource}, SdkType.service_sdk) -@pytest.mark.skipif(not validate_min_python_version(3, 5, exit_on_fail=False), reason="minimum python version not satisfied") -class TestMonitorEvents(): - @pytest.mark.parametrize("timeout, exception", [ - (-1, CLIError), - ]) +@pytest.mark.skipif( + not validate_min_python_version(3, 5, exit_on_fail=False), + reason="minimum python version not satisfied", +) +class TestMonitorEvents: + @pytest.mark.parametrize("timeout, exception", [(-1, CLIError)]) def test_monitor_events_invalid_args(self, timeout, exception, fixture_cmd): with pytest.raises(exception): subject.iot_central_monitor_events(fixture_cmd, app_id, timeout=timeout) diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index a2ed83f86..283dd4641 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -1,12 +1,14 @@ import pytest import json from knack.util import CLIError +from uamqp.message import Message, MessageProperties from azure.cli.core.extension import get_extension_path from azext_iot.common.utility import validate_min_python_version from azext_iot.common.deps import ensure_uamqp from azext_iot._validators import mode2_iot_login_handler from azext_iot.constants import EVENT_LIB, EXTENSION_NAME from azext_iot.common.utility import process_json_arg, read_file_content, logger +from azext_iot.operations.events3 import _parser class TestMinPython(object): @@ -42,7 +44,7 @@ class TestMode2Handler(object): {"dps_name": None, "login": "connection_string"}, {"dps_name": "mydps", "login": "connection_string"}, {"dps_name": None, "login": None}, - {"cmd.name": "webapp", "login": None} + {"cmd.name": "webapp", "login": None}, ] ) def mode2_scenario(self, mocker, request): @@ -226,7 +228,8 @@ def test_inline_json_fail(self, content, argname): ) @pytest.mark.parametrize( - "content, argname", [("iothub/configurations/test_adm_device_content.json", "myarg0")] + "content, argname", + [("iothub/configurations/test_adm_device_content.json", "myarg0")], ) def test_file_json(self, content, argname, set_cwd): result = process_json_arg(content, argument_name=argname) @@ -264,3 +267,216 @@ def test_file_json_fail_invalidcontent(self, content, argname, set_cwd, mocker): ) ) assert mocked_util_logger.call_count == 0 + + +@pytest.mark.skipif( + not validate_min_python_version(3, 5, exit_on_fail=False), + reason="minimum python version not satisfied", +) +class TestEvents3Parser: + device_id = "some-device-id" + payload = {"someProperty": "someValue"} + encoding = "UTF-8" + content_type = "application/json" + + bad_encoding = "ascii" + bad_payload = "bad-payload" + bad_content_type = "bad-content-type" + + def test_parse_message_should_succeed(self): + # setup + app_prop_type = "some_app_prop" + app_prop_value = "some_app_value" + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.payload).encode(), + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + application_properties={app_prop_type.encode(): app_prop_value.encode()}, + ) + parser = _parser.Event3Parser() + + # act + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties={"all"}, + content_type_hint=None, + simulate_errors=False, + ) + + # verify + assert parsed_msg["event"]["payload"] == self.payload + assert parsed_msg["event"]["origin"] == self.device_id + + properties = parsed_msg["event"]["properties"] + device_identifier = str(_parser.DEVICE_ID_IDENTIFIER, "utf8") + assert properties["system"]["content_encoding"] == self.encoding + assert properties["system"]["content_type"] == self.content_type + assert properties["annotations"][device_identifier] == self.device_id + assert properties["application"][app_prop_type] == app_prop_value + + assert len(parser._errors) == 0 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + def test_parse_message_pnp_should_succeed(self): + # setup + interface_name = "interface_name" + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.payload).encode(), + properties=properties, + annotations={ + _parser.DEVICE_ID_IDENTIFIER: self.device_id.encode(), + _parser.INTERFACE_NAME_IDENTIFIER: interface_name.encode(), + }, + ) + parser = _parser.Event3Parser() + + # act + parsed_msg = parser.parse_message( + message=message, + pnp_context=True, + interface_name=interface_name, + properties=None, + content_type_hint=None, + simulate_errors=False, + ) + + # verify + assert parsed_msg["event"]["payload"] == self.payload + assert parsed_msg["event"]["origin"] == self.device_id + assert parsed_msg["event"]["interface"] == interface_name + + assert len(parser._errors) == 0 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + def test_parse_message_bad_content_type_should_warn(self): + # setup + encoded_payload = json.dumps(self.payload).encode() + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.bad_content_type + ) + message = Message( + body=encoded_payload, + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + ) + parser = _parser.Event3Parser() + + # act + parsed_msg = parser.parse_message(message, None, None, None, None, False) + + # verify + # since the content_encoding header is not present, just dump the raw payload + assert parsed_msg["event"]["payload"] == str(encoded_payload, "utf8") + + assert len(parser._errors) == 0 + assert len(parser._warnings) == 1 + assert len(parser._info) == 0 + + warning = parser._warnings[0] + assert "Content type not supported." in warning + assert self.bad_content_type in warning + assert "application/json" in warning + assert self.device_id in warning + + def test_parse_message_bad_encoding_should_fail(self): + # setup + properties = MessageProperties( + content_encoding=self.bad_encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.payload).encode(self.bad_encoding), + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + ) + parser = _parser.Event3Parser() + + # act + parser.parse_message(message, None, None, None, None, False) + + assert len(parser._errors) == 1 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + errors = parser._errors[0] + assert "Unsupported encoding detected: '{}'".format(self.bad_encoding) in errors + + def test_parse_message_bad_json_should_fail(self): + # setup + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=self.bad_payload.encode(), + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + ) + parser = _parser.Event3Parser() + + # act + parsed_msg = parser.parse_message(message, None, None, None, None, False) + + # verify + # parsing should attempt to place raw payload into result even if parsing fails + assert parsed_msg["event"]["payload"] == self.bad_payload + + assert len(parser._errors) == 1 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + errors = parser._errors[0] + assert "Invalid JSON format." in errors + assert self.device_id in errors + assert self.bad_payload in errors + + def test_parse_message_pnp_should_fail(self): + # setup + actual_interface_name = "actual_interface_name" + expected_interface_name = "expected_interface_name" + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.payload).encode(), + properties=properties, + annotations={ + _parser.DEVICE_ID_IDENTIFIER: self.device_id.encode(), + _parser.INTERFACE_NAME_IDENTIFIER: actual_interface_name.encode(), + }, + ) + parser = _parser.Event3Parser() + + # act + parsed_msg = parser.parse_message( + message=message, + pnp_context=True, + interface_name=expected_interface_name, + properties=None, + content_type_hint=None, + simulate_errors=False, + ) + + # verify + # all the items should still be parsed and available, but we should have an error + assert parsed_msg["event"]["payload"] == self.payload + assert parsed_msg["event"]["origin"] == self.device_id + assert parsed_msg["event"]["interface"] == actual_interface_name + + assert len(parser._errors) == 1 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + actual_error = parser._errors[0] + expected_error = "Inteface name mismatch. {}. Expected: {}, Actual: {}".format( + self.device_id, expected_interface_name, actual_interface_name + ) + assert actual_error == expected_error diff --git a/dev_requirements b/dev_requirements index 4d4c3c42f..dcd5556c4 100644 --- a/dev_requirements +++ b/dev_requirements @@ -3,6 +3,7 @@ pytest-mock pytest-cov pytest-env pylint +uamqp~=1.2 mock;python_version<'3.3' responses flake8 From 79fe125975b6d30129a079722cafbdee6b7e0cd9 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Fri, 10 Apr 2020 15:52:13 -0700 Subject: [PATCH 010/179] Fixed bug in uamqp import logic (#157) * Fixed bug in uamqp import logic --- azext_iot/operations/central.py | 7 ++++--- azext_iot/operations/events3/__init__.py | 4 ---- azext_iot/operations/hub.py | 14 +++++++++----- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 5c30a042a..14b55ea36 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -10,7 +10,6 @@ from azext_iot.common.shared import SdkType from azext_iot.common.utility import unpack_msrest_error, init_monitoring from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication -from azext_iot.operations import events3 def find_between(s, start, end): @@ -102,15 +101,17 @@ def _events3_runner( yes, central_api_uri, ): + from azext_iot.operations.events3 import _builders, _events + (enqueued_time, properties, timeout, output) = init_monitoring( cmd, timeout, properties, enqueued_time, repair, yes ) - eventHubTarget = events3.EventTargetBuilder().build_central_event_hub_target( + eventHubTarget = _builders.EventTargetBuilder().build_central_event_hub_target( cmd, app_id, central_api_uri ) - events3.executor( + _events.executor( eventHubTarget, consumer_group=consumer_group, enqueued_time=enqueued_time, diff --git a/azext_iot/operations/events3/__init__.py b/azext_iot/operations/events3/__init__.py index 5087691d6..55614acbf 100644 --- a/azext_iot/operations/events3/__init__.py +++ b/azext_iot/operations/events3/__init__.py @@ -3,7 +3,3 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from ._builders import EventTargetBuilder -from ._events import executor, send_c2d_message, monitor_feedback - -__all__ = ["EventTargetBuilder", "executor", "send_c2d_message", "monitor_feedback"] diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index ccda89777..c6fef1ca5 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -29,7 +29,6 @@ process_json_arg, ) from azext_iot._factory import SdkResolver, CloudError -from azext_iot.operations import events3 from azext_iot.operations.generic import _execute_query, _process_top @@ -1858,6 +1857,7 @@ def iot_c2d_message_send( ): from azext_iot.common.deps import ensure_uamqp from azext_iot.common.utility import validate_min_python_version + from azext_iot.operations.events3 import _events validate_min_python_version(3, 4) @@ -1882,7 +1882,7 @@ def iot_c2d_message_send( if user_msg_expiry < now_in_milli: raise CLIError("Message expiry time utc is in the past!") - msg_id, errors = events3.send_c2d_message( + msg_id, errors = _events.send_c2d_message( target=target, device_id=device_id, data=data, @@ -2203,6 +2203,8 @@ def _iot_hub_monitor_events( content_type=None, device_query=None, ): + from azext_iot.operations.events3 import _builders, _events + (enqueued_time, properties, timeout, output) = init_monitoring( cmd, timeout, properties, enqueued_time, repair, yes ) @@ -2220,9 +2222,9 @@ def _iot_hub_monitor_events( cmd, hub_name, resource_group_name, include_events=True, login=login ) - eventHubTarget = events3.EventTargetBuilder().build_iot_hub_target(target) + eventHubTarget = _builders.EventTargetBuilder().build_iot_hub_target(target) - events3.executor( + _events.executor( eventHubTarget, consumer_group=consumer_group, enqueued_time=enqueued_time, @@ -2264,7 +2266,9 @@ def iot_hub_distributed_tracing_update( def _iot_hub_monitor_feedback(target, device_id, wait_on_id): - events3.monitor_feedback( + from azext_iot.operations.events3 import _events + + _events.monitor_feedback( target=target, device_id=device_id, wait_on_id=wait_on_id, token_duration=3600 ) From 69b87aba82da000e4d23364e7e9ca26f11d20a6e Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:43:23 -0700 Subject: [PATCH 011/179] Moved uamqp imports below init_monitoring and ensure_uamqp calls (#158) --- azext_iot/operations/central.py | 4 ++-- azext_iot/operations/hub.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 14b55ea36..14e740261 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -101,12 +101,12 @@ def _events3_runner( yes, central_api_uri, ): - from azext_iot.operations.events3 import _builders, _events - (enqueued_time, properties, timeout, output) = init_monitoring( cmd, timeout, properties, enqueued_time, repair, yes ) + from azext_iot.operations.events3 import _builders, _events + eventHubTarget = _builders.EventTargetBuilder().build_central_event_hub_target( cmd, app_id, central_api_uri ) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index c6fef1ca5..675aa2acf 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -1857,7 +1857,6 @@ def iot_c2d_message_send( ): from azext_iot.common.deps import ensure_uamqp from azext_iot.common.utility import validate_min_python_version - from azext_iot.operations.events3 import _events validate_min_python_version(3, 4) @@ -1882,6 +1881,8 @@ def iot_c2d_message_send( if user_msg_expiry < now_in_milli: raise CLIError("Message expiry time utc is in the past!") + from azext_iot.operations.events3 import _events + msg_id, errors = _events.send_c2d_message( target=target, device_id=device_id, @@ -2203,8 +2204,6 @@ def _iot_hub_monitor_events( content_type=None, device_query=None, ): - from azext_iot.operations.events3 import _builders, _events - (enqueued_time, properties, timeout, output) = init_monitoring( cmd, timeout, properties, enqueued_time, repair, yes ) @@ -2222,6 +2221,8 @@ def _iot_hub_monitor_events( cmd, hub_name, resource_group_name, include_events=True, login=login ) + from azext_iot.operations.events3 import _builders, _events + eventHubTarget = _builders.EventTargetBuilder().build_iot_hub_target(target) _events.executor( From ddeab47c603a964b03c98371bab887785183db43 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 13 Apr 2020 12:08:09 -0700 Subject: [PATCH 012/179] Twin patch fix + misc improvements. (#159) --- .github/PULL_REQUEST_TEMPLATE.md | 3 +- CONTRIBUTING.md | 36 +++---- HISTORY.rst | 11 ++- azext_iot/_help.py | 46 +++++---- azext_iot/_params.py | 12 +++ azext_iot/commands.py | 4 + azext_iot/operations/events3/_parser.py | 2 +- azext_iot/operations/hub.py | 75 ++++++++------ azext_iot/tests/test_iot_ext_int.py | 121 ++++++++++++++++++++++- azext_iot/tests/test_iot_ext_unit.py | 44 +-------- azext_iot/tests/test_iot_utility_unit.py | 11 ++- setup.py | 3 - 12 files changed, 247 insertions(+), 121 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c066459cf..3ee7a39e1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,6 @@ --- This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - Thank you for contributing to the IoT extension! This checklist is used to make sure that common guidelines for a pull request are followed. @@ -10,5 +9,5 @@ This checklist is used to make sure that common guidelines for a pull request ar - [ ] If introducing new functionality or modified behavior, are they backed by unit and integration tests? - [ ] In the same context as above are command names and their parameter definitions accurate? Do help docs have sufficient content? -- [ ] Have **all** unit **and** integration tests passed locally? i.e. `pytest -s /azext_iot/tests` +- [ ] Have **all** unit **and** integration tests passed locally? i.e. `pytest -vv` - [ ] Have static checks passed using the .pylintrc and .flake8 rules? Look at the CI scripts for example usage. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b880cd1e2..54211a07e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ 1. Get Python 3: https://www.python.org/downloads/ -#### Required Repositories +### Required Repositories You must fork and clone the repositories below. Follow the videos and instructions found [here](https://github.com/Azure/azure-cli-dev-tools#setting-up-your-development-environment). @@ -65,7 +65,7 @@ azdev setup -c path/to/source/azure-cli #### Install dev extension -1. Change directories +1. Change directories ```powershell cd path/to/source/azure-iot-cli-extension @@ -89,7 +89,7 @@ If this works, then you should now be able to make changes to the extension and ## Unit and Integration Testing -#### Unit Tests +### Unit Tests You may need to install the dev_requirements for this @@ -103,11 +103,11 @@ _Hub:_ _DPS:_ `pytest azext_iot/tests/test_iot_dps_unit.py` -#### Integration Tests +### Integration Tests Integration tests are run against Azure resources and depend on environment variables. -##### Azure Resource Setup +#### Azure Resource Setup 1. Create IoT Hub @@ -119,7 +119,8 @@ Integration tests are run against Azure resources and depend on environment vari 4. Link IoT Hub to DPS - From DPS, click "Linked IoT Hub" and link the IoT Hub you just created. -##### Environment Variables +#### Integration Test Environment Variables + You can either manually set the environment variables or use the `pytest.ini.example` file in the root of the extension repo. To use that file, rename it to `pytest.ini`, open it and set the variables as indicated below. ``` @@ -133,14 +134,12 @@ You can either manually set the environment variables or use the `pytest.ini.exa `azext_iot_teststorageuri` is optional and only required when you want to test device export and file upload functionality. You can generate a SAS Uri for your Blob container using the [Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/). You must also configure your IoT Hub's File Upload storage container via the Azure Portal for this test to pass. - ##### IoT Hub Execute the following command to run the IoT Hub integration tests: `pytest azext_iot/tests/test_iot_ext_int.py` - ##### Device Provisioning Service Execute the following command to run the IoT Hub DPS integration tests: @@ -163,11 +162,11 @@ https://medium.com/@marcobelo/setting-up-python-black-on-visual-studio-code-5318 ## Optional -#### VSCode setup +### VSCode setup 1. Install VSCode -2. Install the required extensions +2. Install the required extensions * ([ms-python.python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) is recommended) 3. Set up `settings.json` @@ -191,7 +190,7 @@ https://medium.com/@marcobelo/setting-up-python-black-on-visual-studio-code-5318 ``` 4. Set up `launch.json` - + ```json { "version": "0.2.0", @@ -217,22 +216,24 @@ https://medium.com/@marcobelo/setting-up-python-black-on-visual-studio-code-5318 ] } ``` - + * launch.json was derived from [this](https://raw.githubusercontent.com/Azure/azure-cli/dev/.vscode/launch.json) file * Note: your "program" path might be different if you did not set up the folder structure as siblings as recommended above * Note: when passing args, ensure they are all comma separated. - Correct: - ``` + Correct: + + ```json "args": [ "--a", "value", "--b", "value" ], ``` - Incorrect: - ``` + Incorrect: + + ```json "args": [ "--a value --b value" ], @@ -246,12 +247,11 @@ https://medium.com/@marcobelo/setting-up-python-black-on-visual-studio-code-5318 https://docs.python.org/3/library/pdb.html - 1. `pip install pdb` 2. If you need a breakpoint, put `import pdb; pdb.set_trace()` in your code 3. Run your command, it should break execution wherever you put the breakpoint. -# Microsoft CLA +## Microsoft CLA This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us diff --git a/HISTORY.rst b/HISTORY.rst index 176e1f046..030110325 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,16 @@ Release History =============== +0.9.2 ++++++++++++++++ +* Device and module twin update operations provide explicit patch arguments (--desired, --tags). +* Adds command "az iot central app validate-messages" +* Remove Py 2.7 remnants from setup manifest. + +0.9.1 ++++++++++++++++ +* Adds edge configuration argument for creating or updating enrollment[groups] + 0.9.0 +++++++++++++++ * Breaking change: Evaluating an edge deployment/hub configuration SYSTEM metric (via show-metric) will return non-manipulated query output. @@ -14,7 +24,6 @@ Release History * Extension package name has been changed to 'azure-iot'. * Help text for ADM module configurations has been updated with proper target condition syntax for module criteria. - 0.8.9 +++++++++++++++ * Updated uamqp version to ~1.2. diff --git a/azext_iot/_help.py b/azext_iot/_help.py index ecdd3bdd6..c8023777c 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -295,18 +295,23 @@ "iot hub device-twin update" ] = """ type: command - short-summary: Update device twin definition. - long-summary: Use --set followed by property assignments for updating a device twin. - Leverage properties returned from 'iot hub device-twin show'. + short-summary: Update device twin desired properties and tags. + long-summary: Provide --desired or --tags arguments for PATCH behavior. + Usage of generic update args (i.e. --set) will reflect PUT behavior + and are deprecated. examples: - - name: Add nested tags to device twin. + - name: Patch device twin desired properties. text: > - az iot hub device-twin update --device-id {device_id} --hub-name {iothub_name} - --set tags='{"location":{"region":"US"}}' - - name: Remove the 'region' property from parent 'location' property + az iot hub device-twin update -n {iothub_name} -d {device_id} + --desired '{"conditions":{"temperature":{"warning":70, "critical":100}}}' + - name: Patch device twin tags. text: > - az iot hub device-twin update --device-id {device_id} --hub-name {iothub_name} - --set tags.location.region='null' + az iot hub device-twin update -n {iothub_name} -d {device_id} + --tags '{"country": "USA"}}' + - name: Patch removal of 'critical' desired property from parent 'temperature' + text: > + az iot hub device-twin update -n {iothub_name} -d {device_id} + --desired '{"condition":{"temperature":{"critical": null}}}' """ helps[ @@ -396,18 +401,23 @@ "iot hub module-twin update" ] = """ type: command - short-summary: Update module twin definition. - long-summary: Use --set followed by property assignments for updating a module. - Leverage properties returned from 'iot hub module-twin show'. + short-summary: Update module twin desired properties and tags. + long-summary: Provide --desired or --tags arguments for PATCH behavior. + Usage of generic update args (i.e. --set) will reflect PUT behavior + and are deprecated. examples: - - name: Add desired properties to module twin. + - name: Patch module twin desired properties. + text: > + az iot hub module-twin update -n {iothub_name} -d {device_id} -m {module_id} + --desired '{"conditions":{"temperature":{"warning":70, "critical":100}}}' + - name: Patch module twin tags. text: > - az iot hub module-twin update -d {device_id} -n {iothub_name} -m {module_name} --set - properties.desired='{"conditions":{"temperature":{"warning":70, "critical":100}}}' - - name: Remove 'critical' property from parent 'temperature' + az iot hub module-twin update -n {iothub_name} -d {device_id} -m {module_id} + --tags '{"country": "USA"}}' + - name: Patch removal of 'critical' desired property from parent 'temperature' text: > - az iot hub module-twin update -d mydevice -n myhub -m mymod --set - properties.desired.condition.temperature.critical='null' + az iot hub module-twin update -n {iothub_name} -d {device_id} -m {module_id} + --desired '{"condition":{"temperature":{"critical": null}}}' """ helps[ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 322b2b2bb..3ed3569f8 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -252,6 +252,18 @@ def load_arguments(self, _): help="Generate self-signed cert and use its thumbprint. " "Output to specified target directory", ) + context.argument( + "tags", + arg_group="Twin Patch", + options_list=["--tags"], + help="Twin tags." + ) + context.argument( + "desired", + arg_group="Twin Patch", + options_list=["--desired"], + help="Twin desired properties." + ) with self.argument_context("iot hub job") as context: context.argument("job_id", options_list=["--job-id"], help="IoT Hub job Id.") diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 7fdfc0d88..2d34b6d6e 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -73,6 +73,8 @@ def load_command_table(self, _): "update", getter_name="iot_device_module_twin_show", setter_name="iot_device_module_twin_update", + custom_func_name="iot_twin_update_custom", + custom_func_type=iothub_ops, ) with self.command_group( @@ -84,6 +86,8 @@ def load_command_table(self, _): "update", getter_name="iot_device_twin_show", setter_name="iot_device_twin_update", + custom_func_name="iot_twin_update_custom", + custom_func_type=iothub_ops, ) with self.command_group( diff --git a/azext_iot/operations/events3/_parser.py b/azext_iot/operations/events3/_parser.py index 38f7ebd3c..345d5bc10 100644 --- a/azext_iot/operations/events3/_parser.py +++ b/azext_iot/operations/events3/_parser.py @@ -81,7 +81,7 @@ def parse_message( if "anno" in properties or "all" in properties: annotations = self._parse_annotations(message) - event["properties"]["annotations"] = annotations + event["annotations"] = annotations if system_properties and ("sys" in properties or "all" in properties): event["properties"]["system"] = system_properties diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 675aa2acf..b031f8a1a 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -740,27 +740,24 @@ def iot_device_module_twin_update( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - etag = parameters.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - verify = {"properties.desired": dict} - - if parameters.get("tags"): - verify["tags"] = dict - - verify_transform(parameters, verify) - - return service_sdk.twin.update_module_twin( - id=device_id, - mid=module_id, - device_twin_info=parameters, - custom_headers=headers, - ) - raise LookupError("module twin etag not found") + headers = {} + headers["If-Match"] = '"*"' + verify = {} + if parameters.get("properties"): + if parameters["properties"].get("desired"): + verify = {"properties.desired": dict} + if parameters.get("tags"): + verify["tags"] = dict + verify_transform(parameters, verify) + return service_sdk.twin.update_module_twin( + id=device_id, + mid=module_id, + device_twin_info=parameters, + custom_headers=headers, + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except (AttributeError, LookupError, TypeError) as err: + except (AttributeError, TypeError) as err: raise CLIError(err) @@ -1270,6 +1267,20 @@ def _iot_device_twin_show(target, device_id): raise CLIError(unpack_msrest_error(e)) +def iot_twin_update_custom(instance, desired=None, tags=None): + payload = {} + is_patch = False + if desired: + is_patch = True + payload["properties"] = {"desired": process_json_arg(desired, "desired")} + + if tags: + is_patch = True + payload["tags"] = process_json_arg(tags, "tags") + + return payload if is_patch else instance + + def iot_device_twin_update( cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None ): @@ -1282,21 +1293,21 @@ def iot_device_twin_update( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - etag = parameters.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - verify = {"properties.desired": dict} - if parameters.get("tags", None): - verify["tags"] = dict - verify_transform(parameters, verify) - return service_sdk.twin.update_device_twin( - id=device_id, device_twin_info=parameters, custom_headers=headers - ) - raise LookupError("device twin etag not found") + headers = {} + headers["If-Match"] = '"*"' + verify = {} + if parameters.get("properties"): + if parameters["properties"].get("desired"): + verify = {"properties.desired": dict} + if parameters.get("tags"): + verify["tags"] = dict + verify_transform(parameters, verify) + return service_sdk.twin.update_device_twin( + id=device_id, device_twin_info=parameters, custom_headers=headers + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except (AttributeError, LookupError, TypeError) as err: + except (AttributeError, TypeError) as err: raise CLIError(err) diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index bd196988e..77bdec996 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -15,7 +15,9 @@ opt_env_set = ["azext_iot_teststorageuri"] -settings = DynamoSettings(req_env_set=ENV_SET_TEST_IOTHUB_BASIC, opt_env_set=opt_env_set) +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 @@ -492,6 +494,8 @@ def __init__(self, test_case): 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) @@ -529,6 +533,58 @@ def test_hub_device_twins(self): ], ) + # 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], LIVE_HUB_CS, '"{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}"' @@ -607,7 +663,9 @@ def test_hub_device_twins(self): # Region specific test if self.region not in ["West US 2", "North Europe", "Southeast Asia"]: - warnings.warn("Skipping distributed-tracing tests. IoT Hub not in supported region!") + warnings.warn( + "Skipping distributed-tracing tests. IoT Hub not in supported region!" + ) else: self.cmd( "iot hub distributed-tracing show -d {} -n {} -g {}".format( @@ -925,6 +983,8 @@ def __init__(self, test_case): 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 @@ -997,6 +1057,63 @@ def test_hub_module_twins(self): ], ) + # 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"], + ), + ], + ) + + # 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], LIVE_HUB_CS, 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"] + ), + ], + ) + + # 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"] + ), + ], + ) + + # 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}"' diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index 2f7f0f51a..2737da5f9 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -950,25 +950,13 @@ def test_device_twin_update(self, serviceclient, req): assert "twins/{}".format(device_id) in args[0][0].url @pytest.mark.parametrize( - "req, exp", [(generate_device_twin_show(etag=None), CLIError)] - ) - def test_device_twin_update_invalid_args(self, serviceclient, req, exp): - with pytest.raises(exp): - subject.iot_device_twin_update( - fixture_cmd, - req["deviceId"], - hub_name=mock_target["entity"], - parameters=req, - ) - - @pytest.mark.parametrize( - "req", [(generate_device_twin_show()), (generate_device_twin_show(tags=""))] + "req", [generate_device_twin_show()] ) def test_device_twin_update_error(self, serviceclient_generic_error, req): with pytest.raises(CLIError): subject.iot_device_twin_update( fixture_cmd, - req["deviceId"], + device_id=req["deviceId"], hub_name=mock_target["entity"], parameters=req, ) @@ -1063,7 +1051,7 @@ def test_device_module_twin_show_error(self, serviceclient_generic_error): with pytest.raises(CLIError): subject.iot_device_module_twin_show( fixture_cmd, - device_id, + device_id=device_id, hub_name=mock_target["entity"], module_id=module_id, ) @@ -1104,36 +1092,12 @@ def test_device_module_twin_update(self, serviceclient, req): "twins/{}/modules/{}?".format(req["deviceId"], module_id) in args[0][0].url ) - @pytest.mark.parametrize( - "req, exp", - [ - ( - generate_device_twin_show( - moduleId=module_id, - properties={"desired": {"key": "value"}}, - etag=None, - ), - CLIError, - ), - (generate_device_twin_show(moduleId=module_id), CLIError), - ], - ) - def test_device_module_twin_update_invalid_args(self, serviceclient, req, exp): - with pytest.raises(exp): - subject.iot_device_module_twin_update( - fixture_cmd, - req["deviceId"], - hub_name=mock_target["entity"], - module_id=module_id, - parameters=req, - ) - @pytest.mark.parametrize("req", [(generate_device_twin_show(moduleId=module_id))]) def test_device_module_twin_update_error(self, serviceclient_generic_error, req): with pytest.raises(CLIError): subject.iot_device_module_twin_update( fixture_cmd, - req["deviceId"], + device_id=req["deviceId"], hub_name=mock_target["entity"], module_id=module_id, parameters=req, diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index 283dd4641..c6399ca54 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -311,12 +311,12 @@ def test_parse_message_should_succeed(self): # verify assert parsed_msg["event"]["payload"] == self.payload assert parsed_msg["event"]["origin"] == self.device_id + device_identifier = str(_parser.DEVICE_ID_IDENTIFIER, "utf8") + assert parsed_msg["event"]["annotations"][device_identifier] == self.device_id properties = parsed_msg["event"]["properties"] - device_identifier = str(_parser.DEVICE_ID_IDENTIFIER, "utf8") assert properties["system"]["content_encoding"] == self.encoding assert properties["system"]["content_type"] == self.content_type - assert properties["annotations"][device_identifier] == self.device_id assert properties["application"][app_prop_type] == app_prop_value assert len(parser._errors) == 0 @@ -362,7 +362,7 @@ def test_parse_message_bad_content_type_should_warn(self): # setup encoded_payload = json.dumps(self.payload).encode() properties = MessageProperties( - content_encoding=self.encoding, content_type=self.bad_content_type + content_type=self.bad_content_type ) message = Message( body=encoded_payload, @@ -378,7 +378,7 @@ def test_parse_message_bad_content_type_should_warn(self): # since the content_encoding header is not present, just dump the raw payload assert parsed_msg["event"]["payload"] == str(encoded_payload, "utf8") - assert len(parser._errors) == 0 + assert len(parser._errors) == 1 assert len(parser._warnings) == 1 assert len(parser._info) == 0 @@ -388,6 +388,9 @@ def test_parse_message_bad_content_type_should_warn(self): assert "application/json" in warning assert self.device_id in warning + error = parser._errors[0] + assert "No encoding found for message" in error + def test_parse_message_bad_encoding_should_fail(self): # setup properties = MessageProperties( diff --git a/setup.py b/setup.py index f563ba4b1..37e8ee765 100644 --- a/setup.py +++ b/setup.py @@ -56,10 +56,7 @@ "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", From cbd39446756ff93caa775cabcb8c21f1fcb4603b Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Mon, 13 Apr 2020 14:42:00 -0700 Subject: [PATCH 013/179] Fix central int tests mvp. --- azext_iot/tests/test_iot_central_int.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 64ab35e8e..841faf8f8 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -13,7 +13,7 @@ if not all([APP_ID, DEVICE_ID]): raise ValueError( "Set azext_iot_central_app_id " - "and azext_iot_central_device_id to run integration tests. " + "and azext_iot_central_device_id to run central integration tests. " ) @@ -24,14 +24,14 @@ def __init__(self, test_method): def test_central_device_show(self): # Verify incorrect app-id throws error self.cmd( - 'az iotcentral device-twin show --app-id incorrect-app --device-id "{}"'.format( + 'iotcentral device-twin show --app-id incorrect-app --device-id "{}"'.format( DEVICE_ID ), expect_failure=True, ) # Verify incorrect device-id throws error self.cmd( - 'az iotcentral device-twin show --app-id "{}" --device-id incorrect-device'.format( + 'iotcentral device-twin show --app-id "{}" --device-id incorrect-device'.format( APP_ID ), expect_failure=True, @@ -39,7 +39,7 @@ def test_central_device_show(self): # Verify that no errors are thrown when device shown # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices self.cmd( - 'az iotcentral device-twin show --app-id "{}" --device-id "{}"'.format( + 'iotcentral device-twin show --app-id "{}" --device-id "{}"'.format( APP_ID, DEVICE_ID ) ) @@ -52,14 +52,14 @@ def test_central_monitor_events(self): ) # Ensure no failure # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices - self.cmd("iotcentral app monitor-events --app-id {}".format(APP_ID)) + self.cmd("iotcentral app monitor-events --app-id {} --to 1".format(APP_ID)) def test_central_validate_messages(self): # Test with invalid app-id self.cmd( - "iotcentral app validate-messages --app-id {}".format(APP_ID + "zzz"), + "iot central app validate-messages --app-id {}".format(APP_ID + "zzz"), expect_failure=True, ) # Ensure no failure # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices - self.cmd("iotcentral app validate-messages --app-id {}".format(APP_ID)) + self.cmd("iot central app validate-messages --app-id {} --to 1".format(APP_ID)) From f4a825b22fe6078e511634dd4e0f606e8b624226 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Mon, 13 Apr 2020 14:47:00 -0700 Subject: [PATCH 014/179] Clean-up bug template md. --- .github/ISSUE_TEMPLATE/bug_report.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 6b81c6589..7e15e96e0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: Create a report to help us improve -title: "[BUG]" +title: "[bug] " labels: '' assignees: '' @@ -12,6 +12,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,11 +25,12 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** - - OS: [e.g. Windows 10, Ubuntu 16.04, MacOS X] - - Shell: [e.g. bash, wsl bash, powershell, cmd] - - Az CLI version: [e.g. 2.0.80] - - IoT extension version: [e.g. 0.9.1] - - Python version (if pip installed): [e.g. 3.8.1] + +- OS: [e.g. Windows 10, Ubuntu 16.04, MacOS X] +- Shell: [e.g. bash, wsl bash, powershell, cmd] +- Az CLI version: [e.g. 2.0.80] +- IoT extension version: [e.g. 0.9.1] +- Python version (if pip installed): [e.g. 3.8.1] **Additional context** Add any other context about the problem here. From 0ace249d43ca9e813e9809bb6589d01f245f9975 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Mon, 13 Apr 2020 18:48:45 -0700 Subject: [PATCH 015/179] Fix DPS int tests. --- azext_iot/tests/test_iot_dps_int.py | 57 +++++++++++++++-------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/azext_iot/tests/test_iot_dps_int.py b/azext_iot/tests/test_iot_dps_int.py index 34994b19f..a5c16a02d 100644 --- a/azext_iot/tests/test_iot_dps_int.py +++ b/azext_iot/tests/test_iot_dps_int.py @@ -174,20 +174,21 @@ def test_dps_enrollment_symmetrickey_lifecycle(self): ' -g {} --dps-name {} --pk {} --sk {}' ' --provisioning-status {} --device-id {}' ' --initial-twin-tags {} --initial-twin-properties {}' - ' --allocation-policy {} --rp {} --edge-enabled' + ' --allocation-policy {} --rp {} --iot-hubs {} --edge-enabled' .format(enrollment_id, attestation_type, rg, dps, primary_key, secondary_key, self.provisioning_status, device_id, '"{generic_dict}"', '"{generic_dict}"', AllocationType.geolatency.value, - reprovisionPolicy_reprovisionandresetdata,), + reprovisionPolicy_reprovisionandresetdata, + hub_host_name), checks=[ self.check('attestation.type', attestation_type), self.check('registrationId', enrollment_id), self.check('provisioningStatus', self.provisioning_status), self.check('deviceId', device_id), - self.check('allocationPolicy', AllocationType.geolatency.value), - self.check('iotHubHostName', hub_host_name), + self.check('allocationPolicy', 'geoLatency'), + self.check('iotHubs', hub_host_name.split()), self.check('initialTwin.tags', self.kwargs['generic_dict']), self.check('initialTwin.properties.desired', @@ -208,7 +209,7 @@ def test_dps_enrollment_symmetrickey_lifecycle(self): checks=[self.check('registrationId', enrollment_id)]) self.cmd('iot dps enrollment update -g {} --dps-name {} --enrollment-id {}' - ' --provisioning-status {} --etag {} --rc --edge-enabled False' + ' --provisioning-status {} --etag {} --edge-enabled False' .format(rg, dps, enrollment_id, self.provisioning_status_new, etag), checks=[ self.check('attestation.type', attestation_type), @@ -216,11 +217,11 @@ def test_dps_enrollment_symmetrickey_lifecycle(self): self.check('provisioningStatus', self.provisioning_status_new), self.check('deviceId', device_id), - self.check('allocationPolicy', AllocationType.geolatency.value), - self.check('iotHubHostName', hub_host_name), + self.check('allocationPolicy', "geoLatency"), + self.check('iotHubs', hub_host_name.split()), self.exists('initialTwin.tags'), self.exists('initialTwin.properties.desired'), - self.check('attestation.symmetric_key.primary_key', primary_key), + self.check('attestation.symmetricKey.primaryKey', primary_key), self.check('capabilities.iotEdge', False) ]) @@ -235,14 +236,14 @@ def test_dps_enrollment_group_lifecycle(self): ' --cp {} --scp {} --provisioning-status {} --allocation-policy {}' ' --iot-hubs {} --edge-enabled' .format(enrollment_id, rg, dps, cert_path, cert_path, - self.provisioning_status, AllocationType.geolatency.value, + self.provisioning_status, "geoLatency", hub_host_name), checks=[ self.check('enrollmentGroupId', enrollment_id), self.check('provisioningStatus', self.provisioning_status), self.exists('reprovisionPolicy'), - self.check('allocationPolicy', AllocationType.geolatency.value), + self.check('allocationPolicy', "geoLatency"), self.check('iotHubs', hub_host_name.split()), self.check('reprovisionPolicy.migrateDeviceData', True), self.check('reprovisionPolicy.updateHubAssignment', True), @@ -258,23 +259,23 @@ def test_dps_enrollment_group_lifecycle(self): .format(rg, dps, enrollment_id), checks=[self.check('enrollmentGroupId', enrollment_id)]) - self.cmd('iot dps enrollment-group update -g {} --dps-name {} --enrollment-id {}' - ' --provisioning-status {} --rsc --etag {} --rp {} --allocation-policy {}' - '--edge-enabled False' - .format(rg, dps, enrollment_id, self.provisioning_status_new, etag, - reprovisionPolicy_never, AllocationType.hashed.value), - checks=[ - self.check('attestation.type', AttestationType.x509.value), - self.check('enrollmentGroupId', enrollment_id), - self.check('provisioningStatus', self.provisioning_status_new), - self.check('attestation.type.x509.clientCertificates.secondary', None), - self.exists('reprovisionPolicy'), - self.check('allocationPolicy', AllocationType.hashed.value), - self.check('iotHubs', hub_host_name.split()), - self.check('reprovisionPolicy.migrateDeviceData', False), - self.check('reprovisionPolicy.updateHubAssignment', False), - self.check('capabilities.iotEdge', False) - ]) + etag = self.cmd('iot dps enrollment-group update -g {} --dps-name {} --enrollment-id {}' + ' --provisioning-status {} --rsc --etag {} --rp {} --allocation-policy {}' + ' --edge-enabled False --scp {}' + .format(rg, dps, enrollment_id, self.provisioning_status_new, etag, + reprovisionPolicy_never, AllocationType.hashed.value, + cert_path), + checks=[ + self.check('attestation.type', AttestationType.x509.value), + self.check('enrollmentGroupId', enrollment_id), + self.check('provisioningStatus', self.provisioning_status_new), + self.check('attestation.type.x509.clientCertificates.secondary', None), + self.exists('reprovisionPolicy'), + self.check('allocationPolicy', AllocationType.hashed.value), + self.check('reprovisionPolicy.migrateDeviceData', False), + self.check('reprovisionPolicy.updateHubAssignment', False), + self.check('capabilities.iotEdge', False) + ]).get_output_in_json()['etag'] self.cmd('iot dps registration list -g {} --dps-name {} --enrollment-id {}' .format(rg, dps, enrollment_id), @@ -287,7 +288,7 @@ def test_dps_enrollment_group_lifecycle(self): self.cmd('iot dps enrollment-group update -g {} --dps-name {} --enrollment-id {}' ' --cn {} --etag {}' - .format(rg, dps, enrollment_id, cert_name, cert_etag), + .format(rg, dps, enrollment_id, cert_name, etag), checks=[ self.check('attestation.type', AttestationType.x509.value), From 71c46b52d1417c37a562f3767c430ff30e8700df Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 14 Apr 2020 13:17:29 -0700 Subject: [PATCH 016/179] Increment ext version, small correction in help. --- azext_iot/_help.py | 4 ++-- azext_iot/constants.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index c8023777c..d40013c8a 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -307,7 +307,7 @@ - name: Patch device twin tags. text: > az iot hub device-twin update -n {iothub_name} -d {device_id} - --tags '{"country": "USA"}}' + --tags '{"country": "USA"}' - name: Patch removal of 'critical' desired property from parent 'temperature' text: > az iot hub device-twin update -n {iothub_name} -d {device_id} @@ -413,7 +413,7 @@ - name: Patch module twin tags. text: > az iot hub module-twin update -n {iothub_name} -d {device_id} -m {module_id} - --tags '{"country": "USA"}}' + --tags '{"country": "USA"}' - name: Patch removal of 'critical' desired property from parent 'temperature' text: > az iot hub module-twin update -n {iothub_name} -d {device_id} -m {module_id} diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 4054eddc6..be276320f 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.2" +VERSION = "0.9.3" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" From d479c7fe65a9cd9edb9b6256e21078ec22492da8 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 14 Apr 2020 15:25:38 -0700 Subject: [PATCH 017/179] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbe5ea0c1..84a6649d9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The **Azure IoT extension for Azure CLI** aims to accelerate the development, ma ## Commands -Please refer to the official `az iot` page on [Microsoft Docs](https://docs.microsoft.com/en-us/cli/azure/ext/azure-cli-iot-ext) for a complete list of supported commands. You can also find IoT CLI usage tips on the [wiki](https://github.com/Azure/azure-iot-cli-extension/wiki/Tips). +Please refer to the official `az iot` reference on [Microsoft Docs](https://docs.microsoft.com/en-us/cli/azure/ext/azure-iot/iot?view=azure-cli-latest) for a complete list of supported commands. You can also find IoT CLI usage tips on the [wiki](https://github.com/Azure/azure-iot-cli-extension/wiki/Tips). ## Installation From e10bec95cc39b0c60dda2b54f385b7433e3fa01f Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Wed, 15 Apr 2020 19:35:03 -0700 Subject: [PATCH 018/179] Validate field names match DCM (#161) * Adding ability to get DCM given device_id --- azext_iot/_help.py | 11 + azext_iot/central/__init__.py | 5 + azext_iot/central/providers/__init__.py | 9 + .../central/providers/device_provider.py | 80 +++++ azext_iot/central/services/__init__.py | 10 + azext_iot/central/services/_utility.py | 17 ++ azext_iot/central/services/device.py | 48 +++ azext_iot/central/services/device_template.py | 47 +++ azext_iot/commands.py | 7 + azext_iot/common/auth.py | 27 ++ azext_iot/operations/central.py | 61 ++-- azext_iot/operations/events3/_events.py | 15 +- azext_iot/operations/events3/_parser.py | 91 +++++- azext_iot/tests/central/json/device.json | 9 + .../tests/central/json/device_template.json | 287 ++++++++++++++++++ azext_iot/tests/helpers.py | 17 ++ azext_iot/tests/test_constants.py | 10 + azext_iot/tests/test_iot_central_unit.py | 60 +++- azext_iot/tests/test_iot_utility_unit.py | 191 ++++++++++-- 19 files changed, 931 insertions(+), 71 deletions(-) create mode 100644 azext_iot/central/__init__.py create mode 100644 azext_iot/central/providers/__init__.py create mode 100644 azext_iot/central/providers/device_provider.py create mode 100644 azext_iot/central/services/__init__.py create mode 100644 azext_iot/central/services/_utility.py create mode 100644 azext_iot/central/services/device.py create mode 100644 azext_iot/central/services/device_template.py create mode 100644 azext_iot/common/auth.py create mode 100644 azext_iot/tests/central/json/device.json create mode 100644 azext_iot/tests/central/json/device_template.json create mode 100644 azext_iot/tests/helpers.py create mode 100644 azext_iot/tests/test_constants.py diff --git a/azext_iot/_help.py b/azext_iot/_help.py index d40013c8a..1cf0ed68c 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -1247,6 +1247,17 @@ az iot central app validate-messages --app-id {app_id} --simulate-errors """ +helps[ + "iot central app capability-model show" +] = """ + type: command + short-summary: Get the device model from IoT central. + examples: + - name: Basic usage + text: > + az iot central app capability-model show --app-id {app_id} -d {device_id} + """ + helps[ "iot central device-twin" ] = """ diff --git a/azext_iot/central/__init__.py b/azext_iot/central/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/central/__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/central/providers/__init__.py b/azext_iot/central/providers/__init__.py new file mode 100644 index 000000000..21fd79ac1 --- /dev/null +++ b/azext_iot/central/providers/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from .device_provider import CentralDeviceProvider + +__all__ = ["CentralDeviceProvider"] diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py new file mode 100644 index 000000000..330caec21 --- /dev/null +++ b/azext_iot/central/providers/device_provider.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. +# -------------------------------------------------------------------------------------------- + +from knack.util import CLIError +from azext_iot.central import services as central_services + + +class CentralDeviceProvider: + def __init__(self, cmd, app_id, token=None): + """ + Provider for device/device_template APIs + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + Useful in scenarios where user doesn't own the app + therefore AAD token won't work, but a SAS token generated by owner will + """ + self._cmd = cmd + self._app_id = app_id + self._token = token + self._device_templates = {} + self._devices = {} + + def get_device_template( + self, device_id, central_dns_suffix="azureiotcentral.com", + ): + device = self.get_device(device_id, central_dns_suffix) + device_template_urn = device["instanceOf"] + + if not device_template_urn: + raise CLIError( + "No device template urn found for device '{}'".format(device_id) + ) + + # get or add to cache + if ( + device_template_urn not in self._device_templates + or not self._device_templates.get(device_template_urn) + ): + self._device_templates[ + device_template_urn + ] = central_services.device_template.get_device_template( + self._cmd, + device_template_urn, + self._app_id, + self._token, + central_dns_suffix, + ) + + device_template = self._device_templates[device_template_urn] + if not device_template: + raise CLIError( + "No device template for device with id: '{}'.".format(device_id) + ) + + return device_template + + def get_device( + self, device_id, central_dns_suffix="azureiotcentral.com", + ): + if not device_id: + raise CLIError("Device id must be specified.") + + # get or add to cache + if device_id not in self._devices or not self._devices.get(device_id): + self._devices[device_id] = central_services.device.get_device( + self._cmd, device_id, self._app_id, self._token, central_dns_suffix + ) + + device = self._devices[device_id] + if not device: + raise CLIError("No device found with id: '{}'.".format(device_id)) + + return device diff --git a/azext_iot/central/services/__init__.py b/azext_iot/central/services/__init__.py new file mode 100644 index 000000000..6e13f4bad --- /dev/null +++ b/azext_iot/central/services/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from . import device, device_template + + +__all__ = ["device", "device_template"] diff --git a/azext_iot/central/services/_utility.py b/azext_iot/central/services/_utility.py new file mode 100644 index 000000000..5f01196e2 --- /dev/null +++ b/azext_iot/central/services/_utility.py @@ -0,0 +1,17 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# Nothing in this file should be used outside of service/central + +from azext_iot import constants +from azext_iot.common import auth + + +def get_headers(token, cmd): + if not token: + aad_token = auth.get_aad_token(cmd, resource="https://apps.azureiotcentral.com") + token = "Bearer {}".format(aad_token["accessToken"]) + + return {"Authorization": token, "User-Agent": constants.USER_AGENT} diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py new file mode 100644 index 000000000..de96af8a0 --- /dev/null +++ b/azext_iot/central/services/device.py @@ -0,0 +1,48 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devices + +import requests + +from knack.util import CLIError +from . import _utility as utility + + +def get_device( + cmd, + device_id: str, + app_id: str, + token: str, + central_dns_suffix="azureiotcentral.com", +) -> dict: + """ + Get device info given a device id + + Args: + cmd: command passed into az + device_id: unique case-sensitive device id, + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ + + url = "https://{}.{}/api/preview/devices/{}".format( + app_id, central_dns_suffix, device_id + ) + headers = utility.get_headers(token, cmd) + + response = requests.get(url, headers=headers) + + body = response.json() + + if "error" in body: + raise CLIError(body["error"]) + + return body diff --git a/azext_iot/central/services/device_template.py b/azext_iot/central/services/device_template.py new file mode 100644 index 000000000..de239f90a --- /dev/null +++ b/azext_iot/central/services/device_template.py @@ -0,0 +1,47 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# This is largely derived from https://docs.microsoft.com/en-us/rest/api/iotcentral/devicetemplates + +import requests + +from knack.util import CLIError +from . import _utility as utility + + +def get_device_template( + cmd, + device_template_urn: str, + app_id: str, + token: str, + central_dns_suffix="azureiotcentral.com", +) -> dict: + """ + Get device template given a device id + + Args: + cmd: command passed into az + device_template_urn: case sensitive device template urn, + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ + url = "https://{}.{}/api/preview/deviceTemplates/{}".format( + app_id, central_dns_suffix, device_template_urn + ) + headers = utility.get_headers(token, cmd) + + response = requests.get(url, headers=headers) + + body = response.json() + + if "error" in body: + raise CLIError(body["error"]) + + return body diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 2d34b6d6e..fcb88dc99 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -188,6 +188,13 @@ def load_command_table(self, _): "validate-messages", "iot_central_validate_messages", is_preview=True ) + with self.command_group( + "iot central app capability-model", command_type=iotcentral_ops + ) as cmd_group: + cmd_group.command( + "show", "iot_central_device_capability_model_show", is_preview=True + ) + with self.command_group( "iot central device-twin", command_type=iotcentral_ops ) as cmd_group: diff --git a/azext_iot/common/auth.py b/azext_iot/common/auth.py new file mode 100644 index 000000000..a952b81f9 --- /dev/null +++ b/azext_iot/common/auth.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core._profile import Profile + + +def get_aad_token(cmd, resource=None): + """ + get AAD token to access to a specified resource + :param resource: Azure resource endpoints. Default to Azure Resource Manager + Use 'az cloud show' command for other Azure resources + """ + resource = resource or cmd.cli_ctx.cloud.endpoints.active_directory_resource_id + profile = Profile(cli_ctx=cmd.cli_ctx) + creds, subscription, tenant = profile.get_raw_token( + subscription=None, resource=resource + ) + return { + "tokenType": creds[0], + "accessToken": creds[1], + "expiresOn": creds[2].get("expiresOn", "N/A"), + "subscription": subscription, + "tenant": tenant, + } diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 14e740261..a11b903b9 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -10,6 +10,7 @@ from azext_iot.common.shared import SdkType from azext_iot.common.utility import unpack_msrest_error, init_monitoring from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication +from azext_iot.central.providers import CentralDeviceProvider def find_between(s, start, end): @@ -30,6 +31,13 @@ def iot_central_device_show( raise CLIError(unpack_msrest_error(e)) +def iot_central_device_capability_model_show( + cmd, device_id, app_id, central_api_uri="api.azureiotcentral.com" +): + provider = CentralDeviceProvider(cmd, app_id) + return provider.get_device_template(device_id) + + def iot_central_validate_messages( cmd, app_id, @@ -43,19 +51,21 @@ def iot_central_validate_messages( yes=False, central_api_uri="api.azureiotcentral.com", ): + provider = CentralDeviceProvider(cmd, app_id) _events3_runner( - cmd, - app_id, - device_id, - True, - simulate_errors, - consumer_group, - timeout, - enqueued_time, - repair, - properties, - yes, - central_api_uri, + cmd=cmd, + app_id=app_id, + device_id=device_id, + validate_messages=True, + simulate_errors=simulate_errors, + consumer_group=consumer_group, + timeout=timeout, + enqueued_time=enqueued_time, + repair=repair, + properties=properties, + yes=yes, + central_api_uri=central_api_uri, + central_device_provider=provider, ) @@ -72,18 +82,19 @@ def iot_central_monitor_events( central_api_uri="api.azureiotcentral.com", ): _events3_runner( - cmd, - app_id, - device_id, - False, - False, - consumer_group, - timeout, - enqueued_time, - repair, - properties, - yes, - central_api_uri, + cmd=cmd, + app_id=app_id, + device_id=device_id, + validate_messages=False, + simulate_errors=False, + consumer_group=consumer_group, + timeout=timeout, + enqueued_time=enqueued_time, + repair=repair, + properties=properties, + yes=yes, + central_api_uri=central_api_uri, + central_device_provider=None, ) @@ -100,6 +111,7 @@ def _events3_runner( properties, yes, central_api_uri, + central_device_provider, ): (enqueued_time, properties, timeout, output) = init_monitoring( cmd, timeout, properties, enqueued_time, repair, yes @@ -121,4 +133,5 @@ def _events3_runner( output=output, validate_messages=validate_messages, simulate_errors=simulate_errors, + central_device_provider=central_device_provider, ) diff --git a/azext_iot/operations/events3/_events.py b/azext_iot/operations/events3/_events.py index 278ed22ec..0df0a6d12 100644 --- a/azext_iot/operations/events3/_events.py +++ b/azext_iot/operations/events3/_events.py @@ -38,6 +38,7 @@ def executor( pnp_context=None, validate_messages=False, simulate_errors=False, + central_device_provider=None, ): coroutines = [] @@ -56,6 +57,7 @@ def executor( pnp_context, validate_messages, simulate_errors, + central_device_provider, ) ) @@ -113,6 +115,7 @@ async def initiate_event_monitor( pnp_context=None, validate_messages=False, simulate_errors=False, + central_device_provider=None, ): def _get_conn_props(): properties = {} @@ -155,6 +158,7 @@ def _get_conn_props(): pnp_context=pnp_context, validate_messages=validate_messages, simulate_errors=simulate_errors, + central_device_provider=central_device_provider, ) ) return await asyncio.gather(*coroutines, return_exceptions=True) @@ -178,6 +182,7 @@ async def monitor_events( pnp_context=None, validate_messages=False, simulate_errors=False, + central_device_provider=None, ): source = uamqp.address.Source( "amqps://{}/{}/ConsumerGroups/{}/Partitions/{}".format( @@ -214,6 +219,7 @@ async def monitor_events( output, validate_messages, simulate_errors, + central_device_provider, ) except asyncio.CancelledError: @@ -368,6 +374,7 @@ def _output_msg_kpi( output, validate_messages, simulate_errors, + central_device_provider, ): parser = Event3Parser() origin_device_id = parser.parse_device_id(msg) @@ -376,7 +383,13 @@ def _output_msg_kpi( return parsed_msg = parser.parse_message( - msg, pnp_context, interface_name, properties, content_type, simulate_errors + msg, + pnp_context, + interface_name, + properties, + content_type, + simulate_errors, + central_device_provider, ) if output.lower() == "json": diff --git a/azext_iot/operations/events3/_parser.py b/azext_iot/operations/events3/_parser.py index 345d5bc10..72b37c4b3 100644 --- a/azext_iot/operations/events3/_parser.py +++ b/azext_iot/operations/events3/_parser.py @@ -11,6 +11,7 @@ from knack.log import get_logger from uamqp.message import Message from azext_iot.common.utility import parse_entity, unicode_binary_map +from azext_iot.central.providers import CentralDeviceProvider SUPPORTED_ENCODINGS = ["utf-8"] DEVICE_ID_IDENTIFIER = b"iothub-connection-device-id" @@ -36,22 +37,26 @@ def parse_message( properties: dict, content_type_hint: str, simulate_errors: bool, + central_device_provider: CentralDeviceProvider, ) -> dict: self._reset_issues() create_encoding_error = False create_custom_header_warning = False create_payload_error = False + create_payload_name_error = False if not properties: properties = {} # guard against None being passed in - i = random.randint(1, 3) + i = random.randint(1, 4) if simulate_errors and i == 1: create_encoding_error = True if simulate_errors and i == 2: create_custom_header_warning = True if simulate_errors and i == 3: create_payload_error = True + if simulate_errors and i == 4: + create_payload_name_error = True system_properties = self._parse_system_properties(message) @@ -94,6 +99,13 @@ def parse_message( message, origin_device_id, content_type, create_payload_error ) + self._validate_payload_against_dcm( + origin_device_id, + payload, + central_device_provider, + create_payload_name_error, + ) + event["payload"] = payload event_source = {"event": event} @@ -208,6 +220,24 @@ def _parse_content_type( return content_type + def _parse_annotations(self, message: Message): + try: + return unicode_binary_map(message.annotations) + except Exception: + self._warnings.append( + "Unable to decode message.annotations: {}".format(message.annotations) + ) + + def _parse_application_properties(self, message: Message): + try: + return unicode_binary_map(message.application_properties) + except Exception: + self._warnings.append( + "Unable to decode message.application_properties: {}".format( + message.application_properties + ) + ) + def _parse_payload( self, message: Message, origin_device_id, content_type, create_payload_error ): @@ -240,20 +270,57 @@ def _parse_payload( return payload - def _parse_annotations(self, message: Message): + def _validate_payload_against_dcm( + self, + origin_device_id: str, + payload: str, + central_device_provider: CentralDeviceProvider, + create_payload_name_error=False, + ): + if not central_device_provider: + return + + if not hasattr(payload, "keys"): + # some error happend while parsing + # should be captured by _parse_payload method above + return + try: - return unicode_binary_map(message.annotations) - except Exception: - self._warnings.append( - "Unable to decode message.annotations: {}".format(message.annotations) + template = central_device_provider.get_device_template(origin_device_id) + except Exception as e: + self._errors.append( + "Unable to get DCM for device: {}." + "Inner exception: {}".format(origin_device_id, e) ) + return - def _parse_application_properties(self, message: Message): try: - return unicode_binary_map(message.application_properties) + all_schema = self._extract_schema_from_template(template) + all_names = [schema["name"] for schema in all_schema] except Exception: - self._warnings.append( - "Unable to decode message.application_properties: {}".format( - message.application_properties - ) + self._errors.append( + "Unable to extract device schema for device: {}." + "Template: {}".format(origin_device_id, template) ) + return + + for telemetry_name in payload.keys(): + if create_payload_name_error or telemetry_name not in all_names: + self._errors.append( + "Telemetry item '{}' is not present in DCM. " + "Device ID: {}. " + "List of allowed telemetry values for this type of device: {}. " + "NOTE: telemetry names are CASE-SENSITIVE".format( + telemetry_name, origin_device_id, all_names + ) + ) + + def _extract_schema_from_template(self, template): + all_schema = [] + dcm = template["capabilityModel"] + implements = dcm["implements"] + for implementation in implements: + contents = implementation["schema"]["contents"] + all_schema.extend(contents) + + return all_schema diff --git a/azext_iot/tests/central/json/device.json b/azext_iot/tests/central/json/device.json new file mode 100644 index 000000000..0b138571a --- /dev/null +++ b/azext_iot/tests/central/json/device.json @@ -0,0 +1,9 @@ +{ + "id": "device-id", + "etag": "some-etag", + "displayName": "device-id", + "instanceOf": "urn:d9cltbeus:tvj4oal1a0", + "simulated": false, + "provisioned": true, + "approved": true +} \ No newline at end of file diff --git a/azext_iot/tests/central/json/device_template.json b/azext_iot/tests/central/json/device_template.json new file mode 100644 index 000000000..5178f5b18 --- /dev/null +++ b/azext_iot/tests/central/json/device_template.json @@ -0,0 +1,287 @@ +{ + "id": "urn:d9cltbeus:tvj4oal1a0", + "etag": "\"~WgqHZmg+d95gTA53P8AnqBsDLGgj2wa0msOL7xozC9Y=\"", + "types": [ + "DeviceModel" + ], + "displayName": "duplicate-field-name", + "capabilityModel": { + "@id": "urn:sampleApp:modelOne_bz:2", + "@type": [ + "CapabilityModel" + ], + "implements": [ + { + "@id": "urn:sampleApp:modelOne_bz:_rpgcmdpo:1", + "@type": [ + "InterfaceInstance" + ], + "displayName": "Interface", + "name": "modelOne_g4", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:1", + "@type": [ + "Interface" + ], + "displayName": "Interface", + "contents": [ + { + "@id": "urn:sampleApp:modelOne_g4:Bool:1", + "@type": [ + "Telemetry" + ], + "displayName": "Bool", + "name": "Bool", + "schema": "boolean" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Date:1", + "@type": [ + "Telemetry" + ], + "displayName": "Date", + "name": "Date", + "schema": "date" + }, + { + "@id": "urn:sampleApp:modelOne_g4:DateTime:1", + "@type": [ + "Telemetry" + ], + "displayName": "DateTime", + "name": "DateTime", + "schema": "dateTime" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Double:1", + "@type": [ + "Telemetry" + ], + "displayName": "Double", + "name": "Double", + "schema": "double" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Duration:1", + "@type": [ + "Telemetry" + ], + "displayName": "Duration", + "name": "Duration", + "schema": "duration" + }, + { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:1", + "@type": [ + "Telemetry" + ], + "displayName": "IntEnum", + "name": "IntEnum", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:1", + "@type": [ + "Enum" + ], + "displayName": "Enum", + "valueSchema": "integer", + "enumValues": [ + { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum1:1", + "@type": [ + "EnumValue" + ], + "displayName": "Enum1", + "enumValue": 1, + "name": "Enum1" + }, + { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum2:1", + "@type": [ + "EnumValue" + ], + "displayName": "Enum2", + "enumValue": 2, + "name": "Enum2" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:1", + "@type": [ + "Telemetry" + ], + "displayName": "StringEnum", + "name": "StringEnum", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:1", + "@type": [ + "Enum" + ], + "displayName": "Enum", + "valueSchema": "string", + "enumValues": [ + { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumA:1", + "@type": [ + "EnumValue" + ], + "displayName": "EnumA", + "enumValue": "A", + "name": "EnumA" + }, + { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumB:1", + "@type": [ + "EnumValue" + ], + "displayName": "EnumB", + "enumValue": "B", + "name": "EnumB" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_g4:Float:1", + "@type": [ + "Telemetry" + ], + "displayName": "Float", + "name": "Float", + "schema": "float" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Geopoint:1", + "@type": [ + "Telemetry" + ], + "displayName": "Geopoint", + "name": "Geopoint", + "schema": "geopoint" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Int:1", + "@type": [ + "Telemetry" + ], + "displayName": "Int", + "name": "Int", + "schema": "integer" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Long:1", + "@type": [ + "Telemetry" + ], + "displayName": "Long", + "name": "Long", + "schema": "long" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Object:1", + "@type": [ + "Telemetry" + ], + "displayName": "Object", + "name": "Object", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:1", + "@type": [ + "Object" + ], + "displayName": "Object", + "fields": [ + { + "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:Double:1", + "@type": [ + "SchemaField" + ], + "displayName": "Double", + "name": "Double", + "schema": "double" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_g4:String:1", + "@type": [ + "Telemetry" + ], + "displayName": "String", + "name": "String", + "schema": "string" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Time:1", + "@type": [ + "Telemetry" + ], + "displayName": "Time", + "name": "Time", + "schema": "time" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Vector:1", + "@type": [ + "Telemetry" + ], + "displayName": "Vector", + "name": "Vector", + "schema": "vector" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_bz:myxqftpsr:2", + "@type": [ + "InterfaceInstance" + ], + "displayName": "Interface", + "name": "modelTwo_ed", + "schema": { + "@id": "urn:sampleApp:modelTwo_ed:1", + "@type": [ + "Interface" + ], + "displayName": "Interface", + "contents": [ + { + "@id": "urn:sampleApp:modelTwo_ed:Bool:1", + "@type": [ + "Telemetry" + ], + "displayName": "Bool", + "name": "Bool", + "schema": "boolean" + }, + { + "@id": "urn:sampleApp:modelTwo_ed:bool:1", + "@type": [ + "Telemetry" + ], + "displayName": "bool", + "name": "bool", + "schema": "boolean" + } + ] + } + } + ], + "displayName": "larger-telemetry-device", + "@context": [ + "http://azureiot.com/v1/contexts/IoTModel.json" + ] + }, + "solutionModel": { + "@id": "urn:d9cltbeus:lz1tl4a_jz", + "@type": [ + "SolutionModel" + ], + "cloudProperties": [], + "initialValues": [], + "overrides": [] + } +} \ No newline at end of file diff --git a/azext_iot/tests/helpers.py b/azext_iot/tests/helpers.py new file mode 100644 index 000000000..494ba4870 --- /dev/null +++ b/azext_iot/tests/helpers.py @@ -0,0 +1,17 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +import os + +from inspect import getsourcefile + +from azext_iot.common.utility import read_file_content + + +def load_json(filename): + os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) + return json.loads(read_file_content(filename)) diff --git a/azext_iot/tests/test_constants.py b/azext_iot/tests/test_constants.py new file mode 100644 index 000000000..9519aa82e --- /dev/null +++ b/azext_iot/tests/test_constants.py @@ -0,0 +1,10 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +class FileNames: + central_device_template_file = "central/json/device_template.json" + central_device_file = "central/json/device.json" diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index 23b60c533..2365bcef3 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -8,10 +8,13 @@ import pytest from knack.util import CLIError +from azure.cli.core.mock import DummyCli from azext_iot.operations import central as subject from azext_iot.common.shared import SdkType -from azure.cli.core.mock import DummyCli -from azext_iot.common.utility import validate_min_python_version +from azext_iot.central.providers import CentralDeviceProvider + +from .helpers import load_json +from .test_constants import FileNames device_id = "mydevice" @@ -160,12 +163,57 @@ def test_device_twin_show_calls_get_twin( assert args[0] == ({"entity": resource}, SdkType.service_sdk) -@pytest.mark.skipif( - not validate_min_python_version(3, 5, exit_on_fail=False), - reason="minimum python version not satisfied", -) class TestMonitorEvents: @pytest.mark.parametrize("timeout, exception", [(-1, CLIError)]) def test_monitor_events_invalid_args(self, timeout, exception, fixture_cmd): with pytest.raises(exception): subject.iot_central_monitor_events(fixture_cmd, app_id, timeout=timeout) + + +class TestCentralDeviceProvider: + _device = load_json(FileNames.central_device_file) + _device_template = load_json(FileNames.central_device_template_file) + + @mock.patch("azext_iot.central.services.device_template") + @mock.patch("azext_iot.central.services.device") + def test_should_return_device(self, mock_device_svc, mock_device_template_svc): + # setup + provider = CentralDeviceProvider(cmd=None, app_id=app_id) + mock_device_svc.get_device.return_value = self._device + mock_device_template_svc.get_device_template.return_value = ( + self._device_template + ) + + # act + device = provider.get_device("someDeviceId") + # check that caching is working + device = provider.get_device("someDeviceId") + + # verify + # call counts should be at most 1 since the provider has a cache + assert mock_device_svc.get_device.call_count == 1 + assert mock_device_svc.get_device_template.call_count == 0 + assert device == self._device + + @mock.patch("azext_iot.central.services.device_template") + @mock.patch("azext_iot.central.services.device") + def test_should_return_device_template( + self, mock_device_svc, mock_device_template_svc + ): + # setup + provider = CentralDeviceProvider(cmd=None, app_id=app_id) + mock_device_svc.get_device.return_value = self._device + mock_device_template_svc.get_device_template.return_value = ( + self._device_template + ) + + # act + template = provider.get_device_template("someDeviceId") + # check that caching is working + template = provider.get_device_template("someDeviceId") + + # verify + # call counts should be at most 1 since the provider has a cache + assert mock_device_svc.get_device.call_count == 1 + assert mock_device_template_svc.get_device_template.call_count == 1 + assert template == self._device_template diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index c6399ca54..c4143098d 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -1,36 +1,51 @@ -import pytest +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + import json +import mock +import pytest + from knack.util import CLIError from uamqp.message import Message, MessageProperties from azure.cli.core.extension import get_extension_path -from azext_iot.common.utility import validate_min_python_version +from azext_iot.central.providers import CentralDeviceProvider +from azext_iot.common.utility import ( + validate_min_python_version, + process_json_arg, + read_file_content, + logger, +) from azext_iot.common.deps import ensure_uamqp -from azext_iot._validators import mode2_iot_login_handler from azext_iot.constants import EVENT_LIB, EXTENSION_NAME -from azext_iot.common.utility import process_json_arg, read_file_content, logger from azext_iot.operations.events3 import _parser +from azext_iot._validators import mode2_iot_login_handler +from .helpers import load_json +from .test_constants import FileNames class TestMinPython(object): @pytest.mark.parametrize("pymajor, pyminor", [(3, 6), (3, 4), (2, 7)]) - def test_min_python(self, mocker, pymajor, pyminor): - version_mock = mocker.patch("azext_iot.common.utility.sys.version_info") - version_mock.major = pymajor - version_mock.minor = pyminor + def test_min_python(self, pymajor, pyminor): + with mock.patch("azext_iot.common.utility.sys.version_info") as version_mock: + version_mock.major = pymajor + version_mock.minor = pyminor - assert validate_min_python_version(2, 7) + assert validate_min_python_version(2, 7) @pytest.mark.parametrize( "pymajor, pyminor, exception", [(3, 6, SystemExit), (3, 4, SystemExit), (2, 7, SystemExit)], ) - def test_min_python_error(self, mocker, pymajor, pyminor, exception): - version_mock = mocker.patch("azext_iot.common.utility.sys.version_info") - version_mock.major = 2 - version_mock.minor = 6 + def test_min_python_error(self, pymajor, pyminor, exception): + with mock.patch("azext_iot.common.utility.sys.version_info") as version_mock: + version_mock.major = 2 + version_mock.minor = 6 - with pytest.raises(exception): - validate_min_python_version(pymajor, pyminor) + with pytest.raises(exception): + validate_min_python_version(pymajor, pyminor) class TestMode2Handler(object): @@ -269,13 +284,9 @@ def test_file_json_fail_invalidcontent(self, content, argname, set_cwd, mocker): assert mocked_util_logger.call_count == 0 -@pytest.mark.skipif( - not validate_min_python_version(3, 5, exit_on_fail=False), - reason="minimum python version not satisfied", -) class TestEvents3Parser: device_id = "some-device-id" - payload = {"someProperty": "someValue"} + payload = {"String": "someValue"} encoding = "UTF-8" content_type = "application/json" @@ -283,10 +294,12 @@ class TestEvents3Parser: bad_payload = "bad-payload" bad_content_type = "bad-content-type" + bad_dcm_payload = {"temperature": "someValue"} + def test_parse_message_should_succeed(self): # setup - app_prop_type = "some_app_prop" - app_prop_value = "some_app_value" + app_prop_type = "some_property" + app_prop_value = "some_value" properties = MessageProperties( content_encoding=self.encoding, content_type=self.content_type ) @@ -298,6 +311,10 @@ def test_parse_message_should_succeed(self): ) parser = _parser.Event3Parser() + device_template = load_json(FileNames.central_device_template_file) + provider = CentralDeviceProvider(cmd=None, app_id=None) + provider.get_device_template = mock.MagicMock(return_value=device_template) + # act parsed_msg = parser.parse_message( message=message, @@ -306,6 +323,7 @@ def test_parse_message_should_succeed(self): properties={"all"}, content_type_hint=None, simulate_errors=False, + central_device_provider=provider, ) # verify @@ -347,6 +365,7 @@ def test_parse_message_pnp_should_succeed(self): properties=None, content_type_hint=None, simulate_errors=False, + central_device_provider=None, ) # verify @@ -361,9 +380,7 @@ def test_parse_message_pnp_should_succeed(self): def test_parse_message_bad_content_type_should_warn(self): # setup encoded_payload = json.dumps(self.payload).encode() - properties = MessageProperties( - content_type=self.bad_content_type - ) + properties = MessageProperties(content_type=self.bad_content_type) message = Message( body=encoded_payload, properties=properties, @@ -372,7 +389,15 @@ def test_parse_message_bad_content_type_should_warn(self): parser = _parser.Event3Parser() # act - parsed_msg = parser.parse_message(message, None, None, None, None, False) + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties=None, + content_type_hint=None, + simulate_errors=False, + central_device_provider=None, + ) # verify # since the content_encoding header is not present, just dump the raw payload @@ -404,7 +429,15 @@ def test_parse_message_bad_encoding_should_fail(self): parser = _parser.Event3Parser() # act - parser.parse_message(message, None, None, None, None, False) + parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties=None, + content_type_hint=None, + simulate_errors=False, + central_device_provider=None, + ) assert len(parser._errors) == 1 assert len(parser._warnings) == 0 @@ -426,7 +459,15 @@ def test_parse_message_bad_json_should_fail(self): parser = _parser.Event3Parser() # act - parsed_msg = parser.parse_message(message, None, None, None, None, False) + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties=None, + content_type_hint=None, + simulate_errors=False, + central_device_provider=None, + ) # verify # parsing should attempt to place raw payload into result even if parsing fails @@ -466,6 +507,7 @@ def test_parse_message_pnp_should_fail(self): properties=None, content_type_hint=None, simulate_errors=False, + central_device_provider=None, ) # verify @@ -483,3 +525,96 @@ def test_parse_message_pnp_should_fail(self): self.device_id, expected_interface_name, actual_interface_name ) assert actual_error == expected_error + + def test_validate_against_template_should_fail(self): + # setup + app_prop_type = "some_property" + app_prop_value = "some_value" + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.bad_dcm_payload).encode(), + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + application_properties={app_prop_type.encode(): app_prop_value.encode()}, + ) + parser = _parser.Event3Parser() + + device_template = load_json(FileNames.central_device_template_file) + provider = CentralDeviceProvider(cmd=None, app_id=None) + provider.get_device_template = mock.MagicMock(return_value=device_template) + + # act + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties={"all"}, + content_type_hint=None, + simulate_errors=False, + central_device_provider=provider, + ) + + # verify + assert parsed_msg["event"]["payload"] == self.bad_dcm_payload + assert parsed_msg["event"]["origin"] == self.device_id + device_identifier = str(_parser.DEVICE_ID_IDENTIFIER, "utf8") + assert parsed_msg["event"]["annotations"][device_identifier] == self.device_id + + properties = parsed_msg["event"]["properties"] + assert properties["system"]["content_encoding"] == self.encoding + assert properties["system"]["content_type"] == self.content_type + assert properties["application"][app_prop_type] == app_prop_value + + assert len(parser._errors) == 1 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + actual_error = parser._errors[0] + expected_error = "Telemetry item '{}' is not present in DCM.".format( + list(self.bad_dcm_payload)[0] + ) + assert expected_error in actual_error + + def test_validate_against_bad_template_should_not_throw(self): + # setup + app_prop_type = "some_property" + app_prop_value = "some_value" + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.bad_dcm_payload).encode(), + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + application_properties={app_prop_type.encode(): app_prop_value.encode()}, + ) + parser = _parser.Event3Parser() + + provider = CentralDeviceProvider(cmd=None, app_id=None) + provider.get_device_template = mock.MagicMock( + return_value="an_unparseable_template" + ) + + # act + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties={"all"}, + content_type_hint=None, + simulate_errors=False, + central_device_provider=provider, + ) + + # verify + assert parsed_msg["event"]["payload"] == self.bad_dcm_payload + assert parsed_msg["event"]["origin"] == self.device_id + + assert len(parser._errors) == 1 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + actual_error = parser._errors[0] + assert "Unable to extract device schema for device" in actual_error From b5637435e18702d114740f065a978eef7857e97f Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Mon, 20 Apr 2020 18:46:50 -0700 Subject: [PATCH 019/179] Adding methods for devices and device-templates (#164) * Add capability to add/remove/list/get devices and device templates --- azext_iot/__init__.py | 34 +-- azext_iot/central/__init__.py | 4 + azext_iot/central/_help.py | 178 +++++++++++ azext_iot/central/command_map.py | 43 +++ azext_iot/central/commands_device.py | 51 ++++ azext_iot/central/commands_device_template.py | 59 ++++ azext_iot/central/params.py | 48 +++ azext_iot/central/providers/__init__.py | 7 +- .../central/providers/device_provider.py | 131 +++++--- .../providers/device_template_provider.py | 114 +++++++ azext_iot/central/services/__init__.py | 2 +- azext_iot/central/services/_utility.py | 29 +- azext_iot/central/services/device.py | 119 +++++++- azext_iot/central/services/device_template.py | 117 ++++++- azext_iot/operations/central.py | 6 +- azext_iot/operations/events3/_parser.py | 8 +- .../json/device_template_int_test.json | 285 ++++++++++++++++++ .../tests/iothub/jobs/test_iothub_jobs_int.py | 2 +- azext_iot/tests/test_iot_central_int.py | 95 +++++- azext_iot/tests/test_iot_central_unit.py | 12 +- azext_iot/tests/test_iot_utility_unit.py | 10 +- pytest.ini.example | 1 + 22 files changed, 1256 insertions(+), 99 deletions(-) create mode 100644 azext_iot/central/_help.py create mode 100644 azext_iot/central/command_map.py create mode 100644 azext_iot/central/commands_device.py create mode 100644 azext_iot/central/commands_device_template.py create mode 100644 azext_iot/central/params.py create mode 100644 azext_iot/central/providers/device_template_provider.py create mode 100644 azext_iot/tests/central/json/device_template_int_test.json diff --git a/azext_iot/__init__.py b/azext_iot/__init__.py index 4d2cc95d4..e09d53fb4 100644 --- a/azext_iot/__init__.py +++ b/azext_iot/__init__.py @@ -11,51 +11,49 @@ import azext_iot._help # noqa: F401 -iothub_ops = CliCommandType( - operations_tmpl='azext_iot.operations.hub#{}' -) +iothub_ops = CliCommandType(operations_tmpl="azext_iot.operations.hub#{}") -iothub_ops_job = CliCommandType( - operations_tmpl='azext_iot.iothub.job_commands#{}' -) +iothub_ops_job = CliCommandType(operations_tmpl="azext_iot.iothub.job_commands#{}") iothub_ops_device = CliCommandType( - operations_tmpl='azext_iot.iothub.device_commands#{}' + operations_tmpl="azext_iot.iothub.device_commands#{}" ) iotdps_ops = CliCommandType( - operations_tmpl='azext_iot.operations.dps#{}', - client_factory=iot_service_provisioning_factory + operations_tmpl="azext_iot.operations.dps#{}", + client_factory=iot_service_provisioning_factory, ) -iotcentral_ops = CliCommandType( - operations_tmpl='azext_iot.operations.central#{}' -) +iotcentral_ops = CliCommandType(operations_tmpl="azext_iot.operations.central#{}") iotdigitaltwin_ops = CliCommandType( - operations_tmpl='azext_iot.operations.digitaltwin#{}' + operations_tmpl="azext_iot.operations.digitaltwin#{}" ) -iotpnp_ops = CliCommandType( - operations_tmpl='azext_iot.operations.pnp#{}' -) +iotpnp_ops = CliCommandType(operations_tmpl="azext_iot.operations.pnp#{}") class IoTExtCommandsLoader(AzCommandsLoader): - def __init__(self, cli_ctx=None): super(IoTExtCommandsLoader, self).__init__(cli_ctx=cli_ctx) def load_command_table(self, args): from azext_iot.commands import load_command_table - load_command_table(self, args) from azext_iot.iothub.command_bindings import load_iothub_commands + from azext_iot.central.command_map import load_central_commands + + load_command_table(self, args) load_iothub_commands(self, args) + load_central_commands(self, args) + return self.command_table def load_arguments(self, command): from azext_iot._params import load_arguments + from azext_iot.central.params import load_central_arguments + load_arguments(self, command) + load_central_arguments(self, command) COMMAND_LOADER_CLS = IoTExtCommandsLoader diff --git a/azext_iot/central/__init__.py b/azext_iot/central/__init__.py index 55614acbf..73178b8ad 100644 --- a/azext_iot/central/__init__.py +++ b/azext_iot/central/__init__.py @@ -3,3 +3,7 @@ # 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._help import load_central_help + +load_central_help() diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py new file mode 100644 index 000000000..98058f486 --- /dev/null +++ b/azext_iot/central/_help.py @@ -0,0 +1,178 @@ +# 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.help_files import helps + + +def load_central_help(): + helps[ + "iot central" + ] = """ + type: group + short-summary: Manage Azure Central (IoTC) solutions & infrastructure + """ + + _load_central_devices_help() + _load_central_device_templates_help() + + +def _load_central_devices_help(): + helps[ + "iot central app device" + ] = """ + type: group + short-summary: Manage and configure IoTC devices + """ + + helps[ + "iot central app device create" + ] = """ + type: command + short-summary: Create a device in IoTC + + examples: + - name: Create a device + text: > + az iot central app device create + --app-id {appid} + --device-id {deviceid} + + - name: Create a simulated device + text: > + az iot central app device create + --app-id {appid} + --device-id {deviceid} + --instance-of {devicetemplateid} + --simulated + """ + + helps[ + "iot central app device show" + ] = """ + type: command + short-summary: Get a device from IoTC + + examples: + - name: Get a device + text: > + az iot central app device show + --app-id {appid} + --device-id {deviceid} + """ + + helps[ + "iot central app device list" + ] = """ + type: command + short-summary: List all devices in IoTC + + examples: + - name: Get a device + text: > + az iot central app device list + --app-id {appid} + """ + + helps[ + "iot central app device delete" + ] = """ + type: command + short-summary: Delete a device from IoTC + + examples: + - name: Get a device + text: > + az iot central app device list + --app-id {appid} + --device-id {deviceid} + """ + + +def _load_central_device_templates_help(): + helps[ + "iot central app device-template" + ] = """ + type: group + short-summary: Manage and configure IoTC device templates + """ + + helps[ + "iot central app device-template create" + ] = """ + type: command + short-summary: Create a device template in IoTC + + examples: + - name: Create a device with payload read from a file + text: > + az iot central app device create + --app-id {appid} + --content {pathtofile} + --device-template-id {devicetemplateid} + + - name: Create a device with payload read from raw json + text: > + az iot central app device create + --app-id {appid} + --content {json} + --device-template-id {devicetemplateid} + """ + + helps[ + "iot central app device-template show" + ] = """ + type: command + short-summary: Get a device template from IoTC + + examples: + - name: Get a device + text: > + az iot central app device show + --app-id {appid} + --device-template-id {devicetemplateid} + """ + + helps[ + "iot central app device-template list" + ] = """ + type: command + short-summary: List all device templates in IoTC + + examples: + - name: Get a device + text: > + az iot central app device-template list + --app-id {appid} + """ + + helps[ + "iot central app device-template map" + ] = """ + type: command + short-summary: Returns a mapping of device template name to device template id + + examples: + - name: Get device template name to id mapping + text: > + az iot central app device-template map + --app-id {appid} + """ + + helps[ + "iot central app device-template delete" + ] = """ + type: command + short-summary: Delete a device template from IoTC + long-summary: + Note: this is expected to fail + if any devices are still registered to this template. + + examples: + - name: Get a device + text: > + az iot central app device-template list + --app-id {appid} + """ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py new file mode 100644 index 000000000..f4a1250ac --- /dev/null +++ b/azext_iot/central/command_map.py @@ -0,0 +1,43 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +""" +Load CLI commands +""" +from azure.cli.core.commands import CliCommandType + +central_device_ops = CliCommandType( + operations_tmpl="azext_iot.central.commands_device#{}" +) + +central_device_templates_ops = CliCommandType( + operations_tmpl="azext_iot.central.commands_device_template#{}" +) + + +# Dev note - think of this as the "router" and all self.command_group as the controllers +def load_central_commands(self, _): + """ + Load CLI commands + """ + with self.command_group( + "iot central app device", command_type=central_device_ops, is_preview=True, + ) as cmd_group: + cmd_group.command("list", "list_devices") + cmd_group.command("show", "get_device") + cmd_group.command("create", "create_device") + cmd_group.command("delete", "delete_device") + + with self.command_group( + "iot central app device-template", + command_type=central_device_templates_ops, + is_preview=True, + ) as cmd_group: + cmd_group.command("list", "list_device_templates") + cmd_group.command("map", "map_device_templates") + cmd_group.command("show", "get_device_template") + cmd_group.command("create", "create_device_template") + cmd_group.command("delete", "delete_device_template") diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py new file mode 100644 index 000000000..745d92e31 --- /dev/null +++ b/azext_iot/central/commands_device.py @@ -0,0 +1,51 @@ +# 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. +# -------------------------------------------------------------------------------------------- +# Dev note - think of this as a controller + +from knack.util import CLIError +from azext_iot.central.providers import CentralDeviceProvider + + +def list_devices(cmd, app_id: str, central_dns_suffix="azureiotcentral.com"): + provider = CentralDeviceProvider(cmd, app_id) + return provider.list_devices() + + +def get_device( + cmd, app_id: str, device_id: str, central_dns_suffix="azureiotcentral.com" +): + provider = CentralDeviceProvider(cmd, app_id) + return provider.get_device(device_id) + + +def create_device( + cmd, + app_id: str, + device_id: str, + device_name=None, + instance_of=None, + simulated=False, + central_dns_suffix="azureiotcentral.com", +): + if simulated and not instance_of: + raise CLIError( + "Error: if you supply --simulated you must also specify --instance-of" + ) + provider = CentralDeviceProvider(cmd, app_id) + return provider.create_device( + device_id=device_id, + device_name=device_name, + instance_of=instance_of, + simulated=simulated, + central_dns_suffix=central_dns_suffix, + ) + + +def delete_device( + cmd, app_id: str, device_id: str, central_dns_suffix="azureiotcentral.com" +): + provider = CentralDeviceProvider(cmd, app_id) + return provider.delete_device(device_id) diff --git a/azext_iot/central/commands_device_template.py b/azext_iot/central/commands_device_template.py new file mode 100644 index 000000000..2f2050f95 --- /dev/null +++ b/azext_iot/central/commands_device_template.py @@ -0,0 +1,59 @@ +# 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. +# -------------------------------------------------------------------------------------------- +# Dev note - think of this as a controller + +from knack.util import CLIError + +from azext_iot.common import utility +from azext_iot.central.providers import CentralDeviceTemplateProvider + + +def get_device_template( + cmd, app_id: str, device_template_id: str, central_dns_suffix="azureiotcentral.com" +): + provider = CentralDeviceTemplateProvider(cmd, app_id) + return provider.get_device_template( + device_template_id=device_template_id, central_dns_suffix=central_dns_suffix + ) + + +def list_device_templates(cmd, app_id: str, central_dns_suffix="azureiotcentral.com"): + provider = CentralDeviceTemplateProvider(cmd, app_id) + return provider.list_device_templates(central_dns_suffix=central_dns_suffix) + + +def map_device_templates(cmd, app_id: str, central_dns_suffix="azureiotcentral.com"): + provider = CentralDeviceTemplateProvider(cmd, app_id) + return provider.map_device_templates(central_dns_suffix=central_dns_suffix) + + +def create_device_template( + cmd, + app_id: str, + device_template_id: str, + content: str, + central_dns_suffix="azureiotcentral.com", +): + 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, app_id) + return provider.create_device_template( + device_template_id=device_template_id, + payload=payload, + central_dns_suffix=central_dns_suffix, + ) + + +def delete_device_template( + cmd, app_id: str, device_template_id: str, central_dns_suffix="azureiotcentral.com" +): + provider = CentralDeviceTemplateProvider(cmd, app_id) + return provider.delete_device_template( + device_template_id=device_template_id, central_dns_suffix=central_dns_suffix + ) diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py new file mode 100644 index 000000000..a158a969f --- /dev/null +++ b/azext_iot/central/params.py @@ -0,0 +1,48 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Unpublished works. +# -------------------------------------------------------------------------------------------- + +""" +CLI parameter definitions. +""" + +from azure.cli.core.commands.parameters import get_three_state_flag + + +def load_central_arguments(self, _): + """ + Load CLI Args for Knack parser + """ + with self.argument_context("iot central app") as context: + context.argument( + "instance_of", + options_list=["--instance-of"], + help="Central model id. Example: urn:ojpkindbz:modelDefinition:iild3tm_uo", + ) + context.argument( + "device_name", + options_list=["--device-name"], + help="Human readable device name. Example: Fridge", + ) + 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", + ) + context.argument( + "device_template_id", + options_list=["--device-template-id"], + help="Device template id. Example: somedevicetemplate", + ) + context.argument( + "content", + options_list=["--content", "-k"], + help="Configuration for request. " + "Provide path to JSON file or raw stringified JSON. " + "[File Path Example: ./path/to/file.json] " + "[Stringified JSON Example: {'a': 'b'}] ", + ) diff --git a/azext_iot/central/providers/__init__.py b/azext_iot/central/providers/__init__.py index 21fd79ac1..57e791421 100644 --- a/azext_iot/central/providers/__init__.py +++ b/azext_iot/central/providers/__init__.py @@ -4,6 +4,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from .device_provider import CentralDeviceProvider +from azext_iot.central.providers.device_provider import CentralDeviceProvider +from azext_iot.central.providers.device_template_provider import ( + CentralDeviceTemplateProvider, +) -__all__ = ["CentralDeviceProvider"] +__all__ = ["CentralDeviceProvider", "CentralDeviceTemplateProvider"] diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 330caec21..c45becb92 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -9,9 +9,9 @@ class CentralDeviceProvider: - def __init__(self, cmd, app_id, token=None): + def __init__(self, cmd, app_id: str, token=None): """ - Provider for device/device_template APIs + Provider for device APIs Args: cmd: command passed into az @@ -24,57 +24,118 @@ def __init__(self, cmd, app_id, token=None): self._cmd = cmd self._app_id = app_id self._token = token - self._device_templates = {} self._devices = {} + self._device_templates = {} - def get_device_template( + def get_device( self, device_id, central_dns_suffix="azureiotcentral.com", ): - device = self.get_device(device_id, central_dns_suffix) - device_template_urn = device["instanceOf"] - - if not device_template_urn: - raise CLIError( - "No device template urn found for device '{}'".format(device_id) - ) + if not device_id: + raise CLIError("Device id must be specified.") # get or add to cache - if ( - device_template_urn not in self._device_templates - or not self._device_templates.get(device_template_urn) - ): - self._device_templates[ - device_template_urn - ] = central_services.device_template.get_device_template( - self._cmd, - device_template_urn, - self._app_id, - self._token, - central_dns_suffix, + 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, ) + self._devices[device_id] = device - device_template = self._device_templates[device_template_urn] - if not device_template: - raise CLIError( - "No device template for device with id: '{}'.".format(device_id) - ) + if not device: + raise CLIError("No device found with id: '{}'.".format(device_id)) - return device_template + return device - def get_device( + def get_device_template_by_device_id( self, device_id, central_dns_suffix="azureiotcentral.com", ): + from azext_iot.central.providers import CentralDeviceTemplateProvider + if not device_id: raise CLIError("Device id must be specified.") - # get or add to cache - if device_id not in self._devices or not self._devices.get(device_id): - self._devices[device_id] = central_services.device.get_device( - self._cmd, device_id, self._app_id, self._token, central_dns_suffix + device = self.get_device(device_id, central_dns_suffix) + instance_of = device.get("instanceOf") + if not instance_of: + raise CLIError( + "Device '{}' does not have a corresponding device template.".format( + device_id + ) ) - device = self._devices[device_id] + template = CentralDeviceTemplateProvider.get_device_template( + self=self, + device_template_id=instance_of, + central_dns_suffix=central_dns_suffix, + ) + return template + + def list_devices( + self, central_dns_suffix="azureiotcentral.com", + ): + devices = central_services.device.list_devices( + cmd=self._cmd, app_id=self._app_id, token=self._token + ) + + # 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, + instance_of=None, + simulated=False, + central_dns_suffix="azureiotcentral.com", + ): + if not device_id: + raise CLIError("Device id must be specified.") + + 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, + instance_of=instance_of, + simulated=simulated, + token=self._token, + central_dns_suffix=central_dns_suffix, + ) + 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="azureiotcentral.com", + ): + 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, + ) + + # remove from cache + # pop "miss" raises a KeyError if None is not provided + self._devices.pop(device_id, None) + + return result diff --git a/azext_iot/central/providers/device_template_provider.py b/azext_iot/central/providers/device_template_provider.py new file mode 100644 index 000000000..e7bc3ff0a --- /dev/null +++ b/azext_iot/central/providers/device_template_provider.py @@ -0,0 +1,114 @@ +# 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 +from azext_iot.central import services as central_services + + +class CentralDeviceTemplateProvider: + 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="azureiotcentral.com", + ): + # 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, + ) + 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="azureiotcentral.com", + ): + templates = central_services.device_template.list_device_templates( + cmd=self._cmd, app_id=self._app_id, token=self._token + ) + + self._device_templates.update( + {template["id"]: template for template in templates} + ) + + return self._device_templates + + def map_device_templates( + self, central_dns_suffix="azureiotcentral.com", + ): + """ + 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 + ) + return {template["displayName"]: template["id"] for template in templates} + + def create_device_template( + self, + device_template_id: str, + payload: str, + central_dns_suffix="azureiotcentral.com", + ): + 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, + ) + + self._device_templates[template["id"]] = template + + return template + + def delete_device_template( + self, device_template_id, central_dns_suffix="azureiotcentral.com", + ): + 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, + ) + + # 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/services/__init__.py b/azext_iot/central/services/__init__.py index 6e13f4bad..190ad31e8 100644 --- a/azext_iot/central/services/__init__.py +++ b/azext_iot/central/services/__init__.py @@ -4,7 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from . import device, device_template +from azext_iot.central.services import device, device_template __all__ = ["device", "device_template"] diff --git a/azext_iot/central/services/_utility.py b/azext_iot/central/services/_utility.py index 5f01196e2..f3afed651 100644 --- a/azext_iot/central/services/_utility.py +++ b/azext_iot/central/services/_utility.py @@ -5,13 +5,40 @@ # -------------------------------------------------------------------------------------------- # Nothing in this file should be used outside of service/central +from knack.util import CLIError +from requests import Response + from azext_iot import constants from azext_iot.common import auth -def get_headers(token, cmd): +def get_headers(token, cmd, has_json_payload=False): if not token: aad_token = auth.get_aad_token(cmd, resource="https://apps.azureiotcentral.com") token = "Bearer {}".format(aad_token["accessToken"]) + if has_json_payload: + return { + "Authorization": token, + "User-Agent": constants.USER_AGENT, + "Content-Type": "application/json", + } + return {"Authorization": token, "User-Agent": constants.USER_AGENT} + + +def try_extract_result(response: Response): + # 201 and 204 response codes indicate success + # with no content, hence attempting to retrieve content will fail + if response.status_code in [201, 204]: + return {"result": "success"} + + try: + body = response.json() + except: + raise CLIError("Error parsing response body") + + if "error" in body: + raise CLIError(body["error"]) + + return body diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index de96af8a0..46e221bcc 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -8,13 +8,15 @@ import requests from knack.util import CLIError -from . import _utility as utility +from azext_iot.central.services import _utility + +BASE_PATH = "api/preview/devices" def get_device( cmd, - device_id: str, app_id: str, + device_id: str, token: str, central_dns_suffix="azureiotcentral.com", ) -> dict: @@ -33,16 +35,113 @@ def get_device( device: dict """ - url = "https://{}.{}/api/preview/devices/{}".format( - app_id, central_dns_suffix, device_id - ) - headers = utility.get_headers(token, cmd) + url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, device_id) + headers = _utility.get_headers(token, cmd) + + response = requests.get(url, headers=headers) + return _utility.try_extract_result(response) + + +def list_devices( + cmd, app_id: str, token: str, central_dns_suffix="azureiotcentral.com", +) -> list: + """ + Get a list of all devices in IoTC app + + 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 ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + list of devices + """ + + url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) + headers = _utility.get_headers(token, cmd) response = requests.get(url, headers=headers) - body = response.json() + result = _utility.try_extract_result(response) + + if "value" not in result: + raise CLIError("Value is not present in body: {}".format(result)) + + return result["value"] + + +def create_device( + cmd, + app_id: str, + device_id: str, + device_name: str, + instance_of: str, + simulated: bool, + token: str, + central_dns_suffix="azureiotcentral.com", +) -> dict: + """ + Create a device in IoTC - if "error" in body: - raise CLIError(body["error"]) + Args: + cmd: command passed into az + 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 + 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. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ + + if not device_name: + device_name = device_id + + 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) + return _utility.try_extract_result(response) + + +def delete_device( + cmd, + app_id: str, + device_id: str, + token: str, + central_dns_suffix="azureiotcentral.com", +) -> dict: + """ + Delete a device from IoTC + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + device_id: unique case-sensitive device id, + 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": "success"} on success + Raises error on failure + """ + url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, device_id) + headers = _utility.get_headers(token, cmd) - return body + response = requests.delete(url, headers=headers) + 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 de239f90a..46d39889b 100644 --- a/azext_iot/central/services/device_template.py +++ b/azext_iot/central/services/device_template.py @@ -8,22 +8,27 @@ import requests from knack.util import CLIError -from . import _utility as utility +from knack.log import get_logger +from azext_iot.central.services import _utility + +logger = get_logger(__name__) + +BASE_PATH = "api/preview/deviceTemplates" def get_device_template( cmd, - device_template_urn: str, app_id: str, + device_template_id: str, token: str, central_dns_suffix="azureiotcentral.com", ) -> dict: """ - Get device template given a device id + Get a specific device template from IoTC Args: cmd: command passed into az - device_template_urn: case sensitive device template urn, + device_template_id: case sensitive device template id, 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 ...') @@ -32,16 +37,106 @@ def get_device_template( Returns: device: dict """ - url = "https://{}.{}/api/preview/deviceTemplates/{}".format( - app_id, central_dns_suffix, device_template_urn + url = "https://{}.{}/{}/{}".format( + app_id, central_dns_suffix, BASE_PATH, device_template_id ) - headers = utility.get_headers(token, cmd) + headers = _utility.get_headers(token, cmd) response = requests.get(url, headers=headers) + return _utility.try_extract_result(response) + + +def list_device_templates( + cmd, app_id: str, token: str, central_dns_suffix="azureiotcentral.com", +) -> list: + """ + Get a list of all device templates in IoTC + + 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 ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ - body = response.json() + url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) + headers = _utility.get_headers(token, cmd) - if "error" in body: - raise CLIError(body["error"]) + response = requests.get(url, headers=headers) + + result = _utility.try_extract_result(response) + + if "value" not in result: + raise CLIError("Value is not present in body: {}".format(result)) + + return result["value"] + + +def create_device_template( + cmd, + app_id: str, + device_template_id: str, + payload: dict, + token: str, + central_dns_suffix="azureiotcentral.com", +) -> list: + """ + Create a device template in IoTC + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + device_template_id: case sensitive device template id, + payload: see example payload available in + /azext_iot/tests/central/json/device_template_int_test.json + or check here for more information + https://docs.microsoft.com/en-us/rest/api/iotcentral/devicetemplates + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ + + url = "https://{}.{}/{}/{}".format( + app_id, central_dns_suffix, BASE_PATH, device_template_id + ) + headers = _utility.get_headers(token, cmd, has_json_payload=True) + + response = requests.put(url, headers=headers, json=payload) + return _utility.try_extract_result(response) + + +def delete_device_template( + cmd, + app_id: str, + device_template_id: str, + token: str, + central_dns_suffix="azureiotcentral.com", +) -> dict: + """ + Delete a device template from IoTC + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + device_template_id: case sensitive device template id, + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device: dict + """ + url = "https://{}.{}/{}/{}".format( + app_id, central_dns_suffix, BASE_PATH, device_template_id + ) + headers = _utility.get_headers(token, cmd) - return body + response = requests.delete(url, headers=headers) + return _utility.try_extract_result(response) diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index a11b903b9..5870b2f2f 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -32,10 +32,10 @@ def iot_central_device_show( def iot_central_device_capability_model_show( - cmd, device_id, app_id, central_api_uri="api.azureiotcentral.com" + cmd, device_id, app_id, central_dns_suffix="azureiotcentral.com" ): - provider = CentralDeviceProvider(cmd, app_id) - return provider.get_device_template(device_id) + device_provider = CentralDeviceProvider(cmd, app_id) + return device_provider.get_device_template_by_device_id(device_id) def iot_central_validate_messages( diff --git a/azext_iot/operations/events3/_parser.py b/azext_iot/operations/events3/_parser.py index 72b37c4b3..c0a8d3d8f 100644 --- a/azext_iot/operations/events3/_parser.py +++ b/azext_iot/operations/events3/_parser.py @@ -20,12 +20,10 @@ class Event3Parser(object): - _info = [] - _warnings = [] - _errors = [] _logger = get_logger(__name__) def __init__(self, logger=None): + self._reset_issues() if logger: self._logger = logger @@ -286,7 +284,9 @@ def _validate_payload_against_dcm( return try: - template = central_device_provider.get_device_template(origin_device_id) + template = central_device_provider.get_device_template_by_device_id( + origin_device_id + ) except Exception as e: self._errors.append( "Unable to get DCM for device: {}." diff --git a/azext_iot/tests/central/json/device_template_int_test.json b/azext_iot/tests/central/json/device_template_int_test.json new file mode 100644 index 000000000..672320418 --- /dev/null +++ b/azext_iot/tests/central/json/device_template_int_test.json @@ -0,0 +1,285 @@ +{ + "types": [ + "DeviceModel" + ], + "displayName": "int-test-device-template", + "capabilityModel": { + "@id": "urn:sampleApp:modelOne_bz:2", + "@type": [ + "CapabilityModel" + ], + "implements": [ + { + "@id": "urn:sampleApp:modelOne_bz:_rpgcmdpo:1", + "@type": [ + "InterfaceInstance" + ], + "displayName": "Interface", + "name": "modelOne_g4", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:1", + "@type": [ + "Interface" + ], + "displayName": "Interface", + "contents": [ + { + "@id": "urn:sampleApp:modelOne_g4:Bool:1", + "@type": [ + "Telemetry" + ], + "displayName": "Bool", + "name": "Bool", + "schema": "boolean" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Date:1", + "@type": [ + "Telemetry" + ], + "displayName": "Date", + "name": "Date", + "schema": "date" + }, + { + "@id": "urn:sampleApp:modelOne_g4:DateTime:1", + "@type": [ + "Telemetry" + ], + "displayName": "DateTime", + "name": "DateTime", + "schema": "dateTime" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Double:1", + "@type": [ + "Telemetry" + ], + "displayName": "Double", + "name": "Double", + "schema": "double" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Duration:1", + "@type": [ + "Telemetry" + ], + "displayName": "Duration", + "name": "Duration", + "schema": "duration" + }, + { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:1", + "@type": [ + "Telemetry" + ], + "displayName": "IntEnum", + "name": "IntEnum", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:1", + "@type": [ + "Enum" + ], + "displayName": "Enum", + "valueSchema": "integer", + "enumValues": [ + { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum1:1", + "@type": [ + "EnumValue" + ], + "displayName": "Enum1", + "enumValue": 1, + "name": "Enum1" + }, + { + "@id": "urn:sampleApp:modelOne_g4:IntEnum:pgkbdhard:Enum2:1", + "@type": [ + "EnumValue" + ], + "displayName": "Enum2", + "enumValue": 2, + "name": "Enum2" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:1", + "@type": [ + "Telemetry" + ], + "displayName": "StringEnum", + "name": "StringEnum", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:1", + "@type": [ + "Enum" + ], + "displayName": "Enum", + "valueSchema": "string", + "enumValues": [ + { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumA:1", + "@type": [ + "EnumValue" + ], + "displayName": "EnumA", + "enumValue": "A", + "name": "EnumA" + }, + { + "@id": "urn:sampleApp:modelOne_g4:StringEnum:kyesuinpsx:EnumB:1", + "@type": [ + "EnumValue" + ], + "displayName": "EnumB", + "enumValue": "B", + "name": "EnumB" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_g4:Float:1", + "@type": [ + "Telemetry" + ], + "displayName": "Float", + "name": "Float", + "schema": "float" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Geopoint:1", + "@type": [ + "Telemetry" + ], + "displayName": "Geopoint", + "name": "Geopoint", + "schema": "geopoint" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Int:1", + "@type": [ + "Telemetry" + ], + "displayName": "Int", + "name": "Int", + "schema": "integer" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Long:1", + "@type": [ + "Telemetry" + ], + "displayName": "Long", + "name": "Long", + "schema": "long" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Object:1", + "@type": [ + "Telemetry" + ], + "displayName": "Object", + "name": "Object", + "schema": { + "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:1", + "@type": [ + "Object" + ], + "displayName": "Object", + "fields": [ + { + "@id": "urn:sampleApp:modelOne_g4:Object:8ot2x5whp8:Double:1", + "@type": [ + "SchemaField" + ], + "displayName": "Double", + "name": "Double", + "schema": "double" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_g4:String:1", + "@type": [ + "Telemetry" + ], + "displayName": "String", + "name": "String", + "schema": "string" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Time:1", + "@type": [ + "Telemetry" + ], + "displayName": "Time", + "name": "Time", + "schema": "time" + }, + { + "@id": "urn:sampleApp:modelOne_g4:Vector:1", + "@type": [ + "Telemetry" + ], + "displayName": "Vector", + "name": "Vector", + "schema": "vector" + } + ] + } + }, + { + "@id": "urn:sampleApp:modelOne_bz:myxqftpsr:2", + "@type": [ + "InterfaceInstance" + ], + "displayName": "Interface", + "name": "modelTwo_ed", + "schema": { + "@id": "urn:sampleApp:modelTwo_ed:1", + "@type": [ + "Interface" + ], + "displayName": "Interface", + "contents": [ + { + "@id": "urn:sampleApp:modelTwo_ed:Bool:1", + "@type": [ + "Telemetry" + ], + "displayName": "Bool", + "name": "Bool", + "schema": "boolean" + }, + { + "@id": "urn:sampleApp:modelTwo_ed:bool:1", + "@type": [ + "Telemetry" + ], + "displayName": "bool", + "name": "bool", + "schema": "boolean" + } + ] + } + } + ], + "displayName": "larger-telemetry-device", + "@context": [ + "http://azureiot.com/v1/contexts/IoTModel.json" + ] + }, + "solutionModel": { + "@id": "urn:d9cltbeus:lz1tl4a_jz", + "@type": [ + "SolutionModel" + ], + "cloudProperties": [], + "initialValues": [], + "overrides": [] + } +} \ No newline at end of file diff --git a/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py b/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py index e48e30125..1223cb87e 100644 --- a/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py +++ b/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py @@ -168,7 +168,7 @@ def test_jobs(self): # Cancel Job test # Create job to be cancelled - scheduled +7 days from now. - scheduled_time_iso = (datetime.utcnow() + timedelta(days=7)).isoformat() + 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( diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 841faf8f8..524d1b8f0 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -7,19 +7,22 @@ import os from azure.cli.testsdk import LiveScenarioTest +from azext_iot.common import utility + APP_ID = os.environ.get("azext_iot_central_app_id") DEVICE_ID = os.environ.get("azext_iot_central_device_id") +DEVICE_TEMPLATE_PATH = os.environ.get("azext_iot_central_device_template_path") -if not all([APP_ID, DEVICE_ID]): +if not all([APP_ID, DEVICE_ID, DEVICE_TEMPLATE_PATH]): raise ValueError( - "Set azext_iot_central_app_id " - "and azext_iot_central_device_id to run central integration tests. " + "Set azext_iot_central_app_id, azext_iot_central_device_id " + "and azext_iot_central_device_template_path to run central integration tests. " ) class TestIotCentral(LiveScenarioTest): - def __init__(self, test_method): - super(TestIotCentral, self).__init__("test_central_device_show") + def __init__(self, test_case): + super(TestIotCentral, self).__init__(test_case) def test_central_device_show(self): # Verify incorrect app-id throws error @@ -63,3 +66,85 @@ def test_central_validate_messages(self): # Ensure no failure # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices self.cmd("iot central app validate-messages --app-id {} --to 1".format(APP_ID)) + + def test_central_device_methods_CRLD(self): + device_id = self.create_random_name(prefix="aztest", length=24) + device_name = self.create_random_name(prefix="aztest", length=24) + # currently: create, show, list, delete + self.cmd( + "iot central app device create --app-id {} -d {} --device-name {}".format( + APP_ID, device_id, device_name + ), + checks=[ + self.check("approved", True), + self.check("displayName", device_name), + self.check("id", device_id), + self.check("simulated", False), + ], + ) + + self.cmd( + "iot central app device show --app-id {} -d {}".format(APP_ID, device_id), + checks=[ + self.check("approved", True), + self.check("displayName", device_name), + self.check("id", device_id), + self.check("simulated", False), + ], + ) + + list_output = self.cmd("iot central app device list --app-id {}".format(APP_ID)) + + self.cmd( + "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), + checks=[self.check("result", "success")], + ) + + assert device_id in list_output.get_output_in_json() + + def test_central_device_template_methods_CRLD(self): + # currently: create, show, list, delete + template = utility.process_json_arg( + DEVICE_TEMPLATE_PATH, argument_name="DEVICE_TEMPLATE_PATH" + ) + template_name = template["displayName"] + template_id = template_name + "id" + + self.cmd( + "iot central app device-template create --app-id {} --device-template-id {} -k {}".format( + APP_ID, template_id, DEVICE_TEMPLATE_PATH + ), + checks=[ + self.check("displayName", template_name), + self.check("id", template_id), + ], + ) + + self.cmd( + "iot central app device-template show --app-id {} --device-template-id {}".format( + APP_ID, template_id + ), + checks=[ + self.check("displayName", template_name), + self.check("id", template_id), + ], + ) + + list_output = self.cmd( + "iot central app device-template list --app-id {}".format(APP_ID) + ) + map_output = self.cmd( + "iot central app device-template map --app-id {}".format(APP_ID) + ) + + self.cmd( + "iot central app device-template delete --app-id {} --device-template-id {}".format( + APP_ID, template_id + ), + checks=[self.check("result", "success")], + ) + + assert template_id in list_output.get_output_in_json() + + map_json = map_output.get_output_in_json() + assert map_json[template_name] == template_id diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index 2365bcef3..613d87e70 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -11,7 +11,10 @@ from azure.cli.core.mock import DummyCli from azext_iot.operations import central as subject from azext_iot.common.shared import SdkType -from azext_iot.central.providers import CentralDeviceProvider +from azext_iot.central.providers import ( + CentralDeviceProvider, + CentralDeviceTemplateProvider, +) from .helpers import load_json from .test_constants import FileNames @@ -201,19 +204,18 @@ def test_should_return_device_template( self, mock_device_svc, mock_device_template_svc ): # setup - provider = CentralDeviceProvider(cmd=None, app_id=app_id) + provider = CentralDeviceTemplateProvider(cmd=None, app_id=app_id) mock_device_svc.get_device.return_value = self._device mock_device_template_svc.get_device_template.return_value = ( self._device_template ) # act - template = provider.get_device_template("someDeviceId") + template = provider.get_device_template("someDeviceTemplate") # check that caching is working - template = provider.get_device_template("someDeviceId") + template = provider.get_device_template("someDeviceTemplate") # verify # call counts should be at most 1 since the provider has a cache - assert mock_device_svc.get_device.call_count == 1 assert mock_device_template_svc.get_device_template.call_count == 1 assert template == self._device_template diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index c4143098d..9fc75ee09 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -313,7 +313,9 @@ def test_parse_message_should_succeed(self): device_template = load_json(FileNames.central_device_template_file) provider = CentralDeviceProvider(cmd=None, app_id=None) - provider.get_device_template = mock.MagicMock(return_value=device_template) + provider.get_device_template_by_device_id = mock.MagicMock( + return_value=device_template + ) # act parsed_msg = parser.parse_message( @@ -543,7 +545,9 @@ def test_validate_against_template_should_fail(self): device_template = load_json(FileNames.central_device_template_file) provider = CentralDeviceProvider(cmd=None, app_id=None) - provider.get_device_template = mock.MagicMock(return_value=device_template) + provider.get_device_template_by_device_id = mock.MagicMock( + return_value=device_template + ) # act parsed_msg = parser.parse_message( @@ -593,7 +597,7 @@ def test_validate_against_bad_template_should_not_throw(self): parser = _parser.Event3Parser() provider = CentralDeviceProvider(cmd=None, app_id=None) - provider.get_device_template = mock.MagicMock( + provider.get_device_template_by_device_id = mock.MagicMock( return_value="an_unparseable_template" ) diff --git a/pytest.ini.example b/pytest.ini.example index 8708ac4aa..ad6dca94e 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -23,3 +23,4 @@ env = azext_pnp_cs= azext_iot_central_app_id= azext_iot_central_device_id= + azext_iot_central_device_template_path=./azext_iot/tests/central/json/device_template_int_test.json From e5039cdebbc891c10b76bbb3c099db81b78d9bb9 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Tue, 21 Apr 2020 13:28:34 -0700 Subject: [PATCH 020/179] Added ability to retrieve device provisioning state from central and underlying DPS (#167) * Added ability to retrieve device provisioning state from central and underlying DPS --- azext_iot/central/_help.py | 27 ++++++- azext_iot/central/command_map.py | 1 + azext_iot/central/commands_device.py | 40 ++++++++-- azext_iot/central/commands_device_template.py | 31 +++++--- azext_iot/central/params.py | 8 ++ .../central/providers/device_provider.py | 74 ++++++++++++++++++- azext_iot/central/services/device.py | 30 ++++++++ azext_iot/dps/__init__.py | 5 ++ azext_iot/dps/services/__init__.py | 5 ++ azext_iot/dps/services/auth.py | 28 +++++++ azext_iot/dps/services/global_service.py | 46 ++++++++++++ azext_iot/tests/test_iot_central_int.py | 37 ++++++++++ 12 files changed, 312 insertions(+), 20 deletions(-) create mode 100644 azext_iot/dps/__init__.py create mode 100644 azext_iot/dps/services/__init__.py create mode 100644 azext_iot/dps/services/auth.py create mode 100644 azext_iot/dps/services/global_service.py diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 98058f486..fc7263c5a 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -90,6 +90,28 @@ def _load_central_devices_help(): --device-id {deviceid} """ + helps[ + "iot central app device registration-info" + ] = """ + type: command + short-summary: Get registration info on device(s) from IoTC + long-summary: | + Note: This command can take a significant amount of time to return + if no device id is specified and your app contains a lot of devices + + examples: + - name: Get registration info on all devices. This command may take a long time to complete execution. + text: > + az iot central app device registration-info + --app-id {appid} + + - name: Get registration info on specified device + text: > + az iot central app device registration-info + --app-id {appid} + --device-id {deviceid} + """ + def _load_central_device_templates_help(): helps[ @@ -166,9 +188,8 @@ def _load_central_device_templates_help(): ] = """ type: command short-summary: Delete a device template from IoTC - long-summary: - Note: this is expected to fail - if any devices are still registered to this template. + long-summary: | + Note: this is expected to fail if any devices are still registered to this template. examples: - name: Get a device diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index f4a1250ac..996af1352 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -30,6 +30,7 @@ def load_central_commands(self, _): cmd_group.command("show", "get_device") cmd_group.command("create", "create_device") cmd_group.command("delete", "delete_device") + cmd_group.command("registration-info", "registration_info") with self.command_group( "iot central app device-template", diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py index 745d92e31..5933bbb3f 100644 --- a/azext_iot/central/commands_device.py +++ b/azext_iot/central/commands_device.py @@ -9,15 +9,21 @@ from azext_iot.central.providers import CentralDeviceProvider -def list_devices(cmd, app_id: str, central_dns_suffix="azureiotcentral.com"): - provider = CentralDeviceProvider(cmd, app_id) +def list_devices( + cmd, app_id: str, token=None, central_dns_suffix="azureiotcentral.com" +): + provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) return provider.list_devices() def get_device( - cmd, app_id: str, device_id: str, central_dns_suffix="azureiotcentral.com" + cmd, + app_id: str, + device_id: str, + token=None, + central_dns_suffix="azureiotcentral.com", ): - provider = CentralDeviceProvider(cmd, app_id) + provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) return provider.get_device(device_id) @@ -28,13 +34,14 @@ def create_device( device_name=None, instance_of=None, simulated=False, + token=None, central_dns_suffix="azureiotcentral.com", ): if simulated and not instance_of: raise CLIError( "Error: if you supply --simulated you must also specify --instance-of" ) - provider = CentralDeviceProvider(cmd, app_id) + provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) return provider.create_device( device_id=device_id, device_name=device_name, @@ -45,7 +52,26 @@ def create_device( def delete_device( - cmd, app_id: str, device_id: str, central_dns_suffix="azureiotcentral.com" + cmd, + app_id: str, + device_id: str, + token=None, + central_dns_suffix="azureiotcentral.com", ): - provider = CentralDeviceProvider(cmd, app_id) + provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) return provider.delete_device(device_id) + + +def registration_info( + cmd, + app_id: str, + device_id=None, + token=None, + central_dns_suffix="azureiotcentral.com", +): + provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) + if not device_id: + return provider.get_all_registration_info(central_dns_suffix=central_dns_suffix) + return provider.get_device_registration_info( + 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 2f2050f95..8a497a45d 100644 --- a/azext_iot/central/commands_device_template.py +++ b/azext_iot/central/commands_device_template.py @@ -12,21 +12,29 @@ def get_device_template( - cmd, app_id: str, device_template_id: str, central_dns_suffix="azureiotcentral.com" + cmd, + app_id: str, + device_template_id: str, + token=None, + central_dns_suffix="azureiotcentral.com", ): - provider = CentralDeviceTemplateProvider(cmd, app_id) + provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) return provider.get_device_template( device_template_id=device_template_id, central_dns_suffix=central_dns_suffix ) -def list_device_templates(cmd, app_id: str, central_dns_suffix="azureiotcentral.com"): - provider = CentralDeviceTemplateProvider(cmd, app_id) +def list_device_templates( + cmd, app_id: str, token=None, central_dns_suffix="azureiotcentral.com" +): + provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) return provider.list_device_templates(central_dns_suffix=central_dns_suffix) -def map_device_templates(cmd, app_id: str, central_dns_suffix="azureiotcentral.com"): - provider = CentralDeviceTemplateProvider(cmd, app_id) +def map_device_templates( + cmd, app_id: str, token=None, central_dns_suffix="azureiotcentral.com" +): + provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) return provider.map_device_templates(central_dns_suffix=central_dns_suffix) @@ -35,6 +43,7 @@ def create_device_template( app_id: str, device_template_id: str, content: str, + token=None, central_dns_suffix="azureiotcentral.com", ): if not isinstance(content, str): @@ -42,7 +51,7 @@ def create_device_template( payload = utility.process_json_arg(content, argument_name="content") - provider = CentralDeviceTemplateProvider(cmd, app_id) + provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) return provider.create_device_template( device_template_id=device_template_id, payload=payload, @@ -51,9 +60,13 @@ def create_device_template( def delete_device_template( - cmd, app_id: str, device_template_id: str, central_dns_suffix="azureiotcentral.com" + cmd, + app_id: str, + device_template_id: str, + token=None, + central_dns_suffix="azureiotcentral.com", ): - provider = CentralDeviceTemplateProvider(cmd, app_id) + provider = CentralDeviceTemplateProvider(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 ) diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index a158a969f..b39fa4ebe 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -46,3 +46,11 @@ def load_central_arguments(self, _): "[File Path Example: ./path/to/file.json] " "[Stringified JSON Example: {'a': 'b'}] ", ) + context.argument( + "token", + options_list=["--token"], + help="Authorization token for request. " + "More info available here: https://docs.microsoft.com/en-us/learn/modules/manage-iot-central-apps-with-rest-api/ " + "MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...'). " + "Example: 'Bearer someBearerTokenHere'", + ) diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index c45becb92..505574aa9 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -5,7 +5,11 @@ # -------------------------------------------------------------------------------------------- from knack.util import CLIError +from knack.log import get_logger from azext_iot.central import services as central_services +from azext_iot.dps.services import global_service as dps_global_service + +logger = get_logger(__name__) class CentralDeviceProvider: @@ -26,13 +30,14 @@ def __init__(self, cmd, app_id: str, token=None): self._token = token self._devices = {} self._device_templates = {} + self._device_credentials = {} + self._device_registration_info = {} def get_device( self, device_id, central_dns_suffix="azureiotcentral.com", ): if not device_id: raise CLIError("Device id must be specified.") - # get or add to cache device = self._devices.get(device_id) if not device: @@ -137,5 +142,72 @@ def delete_device( # 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 get_device_credentials( + self, device_id, central_dns_suffix="azureiotcentral.com", + ): + credentials = self._device_credentials.get(device_id) + + if not credentials: + credentials = central_services.device.get_device_credentials( + cmd=self._cmd, + app_id=self._app_id, + device_id=device_id, + token=self._token, + ) + + if not credentials: + raise CLIError( + "Could not find device credentials for device '{}'".format(device_id) + ) + + # add to cache + self._device_credentials[device_id] = credentials + + return credentials + + def get_device_registration_info( + self, device_id, central_dns_suffix="azureiotcentral.com", + ): + info = self._device_registration_info.get(device_id) + + if not info: + credentials = self.get_device_credentials( + device_id=device_id, central_dns_suffix=central_dns_suffix + ) + id_scope = credentials["idScope"] + key = credentials["symmetricKey"]["primaryKey"] + dps_state = dps_global_service.get_registration_state( + id_scope=id_scope, key=key, device_id=device_id + ) + central_info = self.get_device(device_id) + info = { + "@device_id": device_id, + "dps_state": dps_state, + "central_info": central_info, + } + + self._device_registration_info[device_id] = info + + return info + + def get_all_registration_info(self, central_dns_suffix="azureiotcentral.com"): + logger.warning("This command may take a long time to complete execution.") + devices = self.list_devices(central_dns_suffix=central_dns_suffix) + real_devices = [ + device for device in devices.values() if not device["simulated"] + ] + if len(devices) != len(real_devices): + logger.warning( + "Getting registration info for following devices. " + "All other devices are simulated. " + "{}".format([device["id"] for device in real_devices]) + ) + result = [ + self.get_device_registration_info(device["id"]) for device in real_devices + ] return result diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index 46e221bcc..717eb89c7 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -145,3 +145,33 @@ def delete_device( response = requests.delete(url, headers=headers) return _utility.try_extract_result(response) + + +def get_device_credentials( + cmd, + app_id: str, + device_id: str, + token: str, + central_dns_suffix="azureiotcentral.com", +): + """ + Get device credentials from IoTC + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + device_id: unique case-sensitive device id, + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + device_credentials: dict + """ + url = "https://{}.{}/{}/{}/credentials".format( + app_id, central_dns_suffix, BASE_PATH, device_id + ) + headers = _utility.get_headers(token, cmd) + + response = requests.get(url, headers=headers) + return _utility.try_extract_result(response) diff --git a/azext_iot/dps/__init__.py b/azext_iot/dps/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/dps/__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/dps/services/__init__.py b/azext_iot/dps/services/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/dps/services/__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/dps/services/auth.py b/azext_iot/dps/services/auth.py new file mode 100644 index 000000000..8ddc1f468 --- /dev/null +++ b/azext_iot/dps/services/auth.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import time +import base64 +import hmac +import hashlib +import urllib + + +def get_dps_sas_auth_header( + scope_id, device_id, key, +): + sr = "{}%2Fregistrations%2F{}".format(scope_id, device_id) + expires = int(time.time() + 21600) + registration_id = f"{sr}\n{str(expires)}" + secret = base64.b64decode(key) + signature = base64.b64encode( + hmac.new( + secret, msg=registration_id.encode("utf8"), digestmod=hashlib.sha256 + ).digest() + ) + quote_signature = urllib.parse.quote(signature, "~()*!.'") + token = f"SharedAccessSignature sr={sr}&sig={quote_signature}&se={str(expires)}&skn=registration" + return token diff --git a/azext_iot/dps/services/global_service.py b/azext_iot/dps/services/global_service.py new file mode 100644 index 000000000..bd972dcf1 --- /dev/null +++ b/azext_iot/dps/services/global_service.py @@ -0,0 +1,46 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# This is for calls that route to the global DPS +# Useful for when you don't know what the dps name is ahead of time +# E.g. most IoT Central scenarios + +import requests + +from azext_iot.dps.services import auth + + +def get_registration_state(id_scope: str, key: str, device_id: str): + """ + Gets device registration state from global dps endpoint + Usefule for when dps name is unknown + + https://docs.microsoft.com/en-us/rest/api/iot-dps/getdeviceregistrationstate/getdeviceregistrationstate + + Params: + id_scope: dps id_scope + key: either primary or secondary symmetric key + device_id: device id that uniquely identifies the device + + Returns: + DeviceRegistrationState: dict + ProvisioningServiceErrorDetails: dict + """ + authToken = auth.get_dps_sas_auth_header(id_scope, device_id, key) + + url = "https://global.azure-devices-provisioning.net/{}/registrations/{}?api-version=2019-03-31".format( + id_scope, device_id + ) + header_parameters = {} + header_parameters["Content-Type"] = "application/json" + header_parameters["Authorization"] = "{}".format(authToken) + body = {"registrationId": "{}".format(device_id)} + response = requests.post(url, headers=header_parameters, json=body) + + try: + response.raise_for_status() + return response.json() + except Exception as e: + return {"error": str(e), "device_id": device_id} diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 524d1b8f0..094f5c1d4 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -5,6 +5,8 @@ # -------------------------------------------------------------------------------------------- import os +import time + from azure.cli.testsdk import LiveScenarioTest from azext_iot.common import utility @@ -148,3 +150,38 @@ def test_central_device_template_methods_CRLD(self): map_json = map_output.get_output_in_json() assert map_json[template_name] == template_id + + def test_central_device_registration_info(self): + device_id = self.create_random_name(prefix="aztest", length=24) + device_name = self.create_random_name(prefix="aztest", length=24) + # currently: create, show, list, delete + self.cmd( + "iot central app device create --app-id {} -d {} --device-name {}".format( + APP_ID, device_id, device_name + ), + checks=[ + self.check("approved", True), + self.check("displayName", device_name), + self.check("id", device_id), + self.check("simulated", False), + ], + ) + + result = self.cmd( + "iot central app device registration-info --app-id {} -d {}".format( + APP_ID, device_id + ) + ) + + self.cmd( + "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), + checks=[self.check("result", "success")], + ) + + json_result = result.get_output_in_json() + assert json_result["@device_id"] == device_id + + # since time taken for provisioning to complete is not known + # we can only assert that the payload is populated, not anything specific beyond that + assert json_result["central_info"] is not None + assert json_result["dps_state"] is not None From 1686c7f2f84061b3c489cbfcae19942b0f65e934 Mon Sep 17 00:00:00 2001 From: Rogelio Delgadillo <35089121+r-delgadillo@users.noreply.github.com> Date: Tue, 21 Apr 2020 19:26:43 -0700 Subject: [PATCH 021/179] Deprecate iotcentral command group (#166) * Deprecate iot central Co-authored-by: rodelga --- azext_iot/commands.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index fcb88dc99..c40f8697d 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -164,11 +164,16 @@ def load_command_table(self, _): cmd_group.command("show", "iot_dps_registration_get") cmd_group.command("delete", "iot_dps_registration_delete") + with self.command_group( + 'iotcentral', command_type=iotcentral_ops, + deprecate_info=self.deprecate(redirect='iot central', hide=True) + ) as cmd_group: + pass + with self.command_group("iotcentral app", command_type=iotcentral_ops) as cmd_group: cmd_group.command( "monitor-events", "iot_central_monitor_events", - deprecate_info="az iot central app monitor-events", ) with self.command_group( @@ -177,7 +182,6 @@ def load_command_table(self, _): cmd_group.command( "show", "iot_central_device_show", - deprecate_info="az iot central device-twin", ) with self.command_group( From 78be22357ec5d0379257ef43253da68e3065a89d Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Tue, 21 Apr 2020 19:40:20 -0700 Subject: [PATCH 022/179] Validate messages command now validates depth=1 field names (#168) * Validate messages command now validates depth=1 field names * remove time module from IT, add user agent to dps * added help for central_dns_suffix * fixed lint issues, removed capability-model command * added linting guidance to CONTRIBUTING.md --- CONTRIBUTING.md | 16 +++++++++- azext_iot/_help.py | 11 ------- azext_iot/central/_help.py | 25 ++++++++-------- azext_iot/central/params.py | 6 ++++ azext_iot/commands.py | 7 ----- azext_iot/dps/services/global_service.py | 9 ++++-- azext_iot/operations/central.py | 7 ----- azext_iot/operations/events3/_parser.py | 26 ++++++++++++++++ azext_iot/tests/test_iot_central_int.py | 1 - azext_iot/tests/test_iot_utility_unit.py | 38 ++++++++++++++++++++++++ 10 files changed, 104 insertions(+), 42 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54211a07e..e59ca2693 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -154,9 +154,23 @@ Execute the following command to run both Unit and Integration tests and output #### Formatting and Linting +The repo uses the linter in `azdev`. + +To install the required version of azdev, run this command: + +```powershell +pip install -e "git+https://github.com/Azure/azure-cli@dev#egg=azure-cli-dev-tools&subdirectory=tools" +``` + +To run the linter, run this command: + +```powershell +azdev cli-lint --ci --extensions azure-iot +``` + We use our flake8 and pylint rules. We recommend you set up your IDE as per the VSCode setup below for best compliance. -We are also starting to use `python black`. To set this up on VSCode, see the following blog post +We are also starting to use `python black`. To set this up on VSCode, see the following blog post. https://medium.com/@marcobelo/setting-up-python-black-on-visual-studio-code-5318eba4cd00 diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 1cf0ed68c..d40013c8a 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -1247,17 +1247,6 @@ az iot central app validate-messages --app-id {app_id} --simulate-errors """ -helps[ - "iot central app capability-model show" -] = """ - type: command - short-summary: Get the device model from IoT central. - examples: - - name: Basic usage - text: > - az iot central app capability-model show --app-id {app_id} -d {device_id} - """ - helps[ "iot central device-twin" ] = """ diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index fc7263c5a..53ddee895 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -70,7 +70,7 @@ def _load_central_devices_help(): short-summary: List all devices in IoTC examples: - - name: Get a device + - name: List all devices in IoTC text: > az iot central app device list --app-id {appid} @@ -83,9 +83,9 @@ def _load_central_devices_help(): short-summary: Delete a device from IoTC examples: - - name: Get a device + - name: Delete a device text: > - az iot central app device list + az iot central app device delete --app-id {appid} --device-id {deviceid} """ @@ -128,16 +128,16 @@ def _load_central_device_templates_help(): short-summary: Create a device template in IoTC examples: - - name: Create a device with payload read from a file + - name: Create a device template with payload read from a file text: > - az iot central app device create + az iot central app device-template create --app-id {appid} --content {pathtofile} --device-template-id {devicetemplateid} - - name: Create a device with payload read from raw json + - name: Create a device template with payload read from raw json text: > - az iot central app device create + az iot central app device-template create --app-id {appid} --content {json} --device-template-id {devicetemplateid} @@ -150,9 +150,9 @@ def _load_central_device_templates_help(): short-summary: Get a device template from IoTC examples: - - name: Get a device + - name: Get a device template text: > - az iot central app device show + az iot central app device-template show --app-id {appid} --device-template-id {devicetemplateid} """ @@ -164,7 +164,7 @@ def _load_central_device_templates_help(): short-summary: List all device templates in IoTC examples: - - name: Get a device + - name: List all device templates text: > az iot central app device-template list --app-id {appid} @@ -192,8 +192,9 @@ def _load_central_device_templates_help(): Note: this is expected to fail if any devices are still registered to this template. examples: - - name: Get a device + - name: Delete a device template from IoTC text: > - az iot central app device-template list + az iot central app device-template delete --app-id {appid} + --device-template-id {devicetemplateid} """ diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index b39fa4ebe..b9f8aa1af 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -54,3 +54,9 @@ def load_central_arguments(self, _): "MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...'). " "Example: 'Bearer someBearerTokenHere'", ) + context.argument( + "central_dns_suffix", + options_list=["--central-dns-suffix"], + help="Central dns suffix. " + "This enables running cli commands against non public/prod environments", + ) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index c40f8697d..b108f4104 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -192,13 +192,6 @@ def load_command_table(self, _): "validate-messages", "iot_central_validate_messages", is_preview=True ) - with self.command_group( - "iot central app capability-model", command_type=iotcentral_ops - ) as cmd_group: - cmd_group.command( - "show", "iot_central_device_capability_model_show", is_preview=True - ) - with self.command_group( "iot central device-twin", command_type=iotcentral_ops ) as cmd_group: diff --git a/azext_iot/dps/services/global_service.py b/azext_iot/dps/services/global_service.py index bd972dcf1..6f1dd0904 100644 --- a/azext_iot/dps/services/global_service.py +++ b/azext_iot/dps/services/global_service.py @@ -9,6 +9,7 @@ import requests +from azext_iot import constants from azext_iot.dps.services import auth @@ -33,9 +34,11 @@ def get_registration_state(id_scope: str, key: str, device_id: str): url = "https://global.azure-devices-provisioning.net/{}/registrations/{}?api-version=2019-03-31".format( id_scope, device_id ) - header_parameters = {} - header_parameters["Content-Type"] = "application/json" - header_parameters["Authorization"] = "{}".format(authToken) + header_parameters = { + "Content-Type": "application/json", + "User-Agent": constants.USER_AGENT, + "Authorization": authToken, + } body = {"registrationId": "{}".format(device_id)} response = requests.post(url, headers=header_parameters, json=body) diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 5870b2f2f..7b23688cb 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -31,13 +31,6 @@ def iot_central_device_show( raise CLIError(unpack_msrest_error(e)) -def iot_central_device_capability_model_show( - cmd, device_id, app_id, central_dns_suffix="azureiotcentral.com" -): - device_provider = CentralDeviceProvider(cmd, app_id) - return device_provider.get_device_template_by_device_id(device_id) - - def iot_central_validate_messages( cmd, app_id, diff --git a/azext_iot/operations/events3/_parser.py b/azext_iot/operations/events3/_parser.py index c0a8d3d8f..22d5a12e5 100644 --- a/azext_iot/operations/events3/_parser.py +++ b/azext_iot/operations/events3/_parser.py @@ -104,6 +104,8 @@ def parse_message( create_payload_name_error, ) + self._validate_field_names(origin_device_id, payload) + event["payload"] = payload event_source = {"event": event} @@ -324,3 +326,27 @@ def _extract_schema_from_template(self, template): all_schema.extend(contents) return all_schema + + def _validate_field_names(self, origin_device_id: str, payload: dict): + # if its not a dictionary, something else went wrong with parsing + if not isinstance(payload, dict): + return + + # source: + # https://github.com/Azure/IoTPlugandPlay/tree/master/DTDL + regex = "^[a-zA-Z_][a-zA-Z0-9_]*$" + + # if a field name does not match the above regex, it is an invalid field name + invalid_field_names = [ + field_name + for field_name in payload.keys() + if not re.search(regex, field_name) + ] + if invalid_field_names: + self._errors.append( + "The following field names are not allowed: '{}'. " + "Payload: '{}'. " + "Message origin: '{}'.".format( + invalid_field_names, payload, origin_device_id + ) + ) diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 094f5c1d4..407b5e739 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -5,7 +5,6 @@ # -------------------------------------------------------------------------------------------- import os -import time from azure.cli.testsdk import LiveScenarioTest diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index 9fc75ee09..5333b6447 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -292,6 +292,7 @@ class TestEvents3Parser: bad_encoding = "ascii" bad_payload = "bad-payload" + bad_field_name = {"violates-regex": "someValue"} bad_content_type = "bad-content-type" bad_dcm_payload = {"temperature": "someValue"} @@ -484,6 +485,43 @@ def test_parse_message_bad_json_should_fail(self): assert self.device_id in errors assert self.bad_payload in errors + def test_parse_message_bad_field_name_should_fail(self): + # setup + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.bad_field_name).encode(), + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + ) + parser = _parser.Event3Parser() + + # act + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties=None, + content_type_hint=None, + simulate_errors=False, + central_device_provider=None, + ) + + # verify + # parsing should attempt to place raw payload into result even if parsing fails + assert parsed_msg["event"]["payload"] == self.bad_field_name + + assert len(parser._errors) == 1 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + errors = parser._errors[0] + assert "The following field names are not allowed" in errors + assert "{}".format(next(iter(self.bad_field_name))) in errors + assert str(self.bad_field_name) in errors + assert self.device_id in errors + def test_parse_message_pnp_should_fail(self): # setup actual_interface_name = "actual_interface_name" From 8f9bd059bb9f4f90917f3908e8b83b7ffe976fb6 Mon Sep 17 00:00:00 2001 From: Jack Barker Date: Tue, 28 Apr 2020 17:18:41 -0700 Subject: [PATCH 023/179] Updated to support multihub in iot central (#162) * Updated to support multihub in iot central * Added newline to end of _events * Updated help and error information for iot central token * Updated re PR comments. Moved device-twin to app device-twin * Added central test for app device-twin * Added additional help to errors --- azext_iot/_help.py | 21 +- azext_iot/_params.py | 7 +- azext_iot/commands.py | 22 +- azext_iot/common/_azure.py | 280 ++++++++++++---------- azext_iot/operations/central.py | 47 ++-- azext_iot/operations/events3/_builders.py | 160 ++++++++----- azext_iot/operations/events3/_events.py | 75 ++++-- azext_iot/tests/test_iot_central_int.py | 22 ++ azext_iot/tests/test_iot_central_unit.py | 65 ++--- 9 files changed, 421 insertions(+), 278 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index d40013c8a..c8ce24f1e 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -1180,7 +1180,12 @@ "iot central app" ] = """ type: group - short-summary: Manage Azure IoT Central applications. + short-summary: | + Manage Azure IoT Central applications. + + To use this command group, the user must be logged through the `az login` command, + have the correct tenant set (the users home tenant) and + have access to the application through http://apps.azureiotcentral.com" """ helps[ @@ -1261,6 +1266,20 @@ short-summary: Get the device twin from IoT Hub. """ +helps[ + "iot central app device-twin" +] = """ + type: group + short-summary: Manage IoT Central device twins. +""" + +helps[ + "iot central app device-twin show" +] = """ + type: command + short-summary: Get the device twin from IoT Hub. +""" + helps[ "iot dt" ] = """ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 3ed3569f8..3811523c2 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -253,16 +253,13 @@ def load_arguments(self, _): "Output to specified target directory", ) context.argument( - "tags", - arg_group="Twin Patch", - options_list=["--tags"], - help="Twin tags." + "tags", arg_group="Twin Patch", options_list=["--tags"], help="Twin tags." ) context.argument( "desired", arg_group="Twin Patch", options_list=["--desired"], - help="Twin desired properties." + help="Twin desired properties.", ) with self.argument_context("iot hub job") as context: diff --git a/azext_iot/commands.py b/azext_iot/commands.py index b108f4104..7ca9541c7 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -165,23 +165,22 @@ def load_command_table(self, _): cmd_group.command("delete", "iot_dps_registration_delete") with self.command_group( - 'iotcentral', command_type=iotcentral_ops, - deprecate_info=self.deprecate(redirect='iot central', hide=True) + "iotcentral", + command_type=iotcentral_ops, + deprecate_info=self.deprecate(redirect="iot central", hide=True), ) as cmd_group: pass with self.command_group("iotcentral app", command_type=iotcentral_ops) as cmd_group: cmd_group.command( - "monitor-events", - "iot_central_monitor_events", + "monitor-events", "iot_central_monitor_events", ) with self.command_group( "iotcentral device-twin", command_type=iotcentral_ops ) as cmd_group: cmd_group.command( - "show", - "iot_central_device_show", + "show", "iot_central_device_show", ) with self.command_group( @@ -194,6 +193,17 @@ def load_command_table(self, _): with self.command_group( "iot central device-twin", command_type=iotcentral_ops + ) as cmd_group: + cmd_group.command( + "show", + "iot_central_device_show", + deprecate_info=self.deprecate( + redirect="iot central app device-twin", hide=True + ), + ) + + with self.command_group( + "iot central app device-twin", command_type=iotcentral_ops ) as cmd_group: cmd_group.command("show", "iot_central_device_show") diff --git a/azext_iot/common/_azure.py b/azext_iot/common/_azure.py index bc7c8d1d2..1fe5540c2 100644 --- a/azext_iot/common/_azure.py +++ b/azext_iot/common/_azure.py @@ -8,68 +8,55 @@ from azext_iot.common.utility import validate_key_value_pairs from azext_iot.common.utility import trim_from_start from azext_iot._factory import iot_hub_service_factory -from azure.cli.core._profile import Profile - - -def _get_aad_token(cmd, resource=None): - ''' - get AAD token to access to a specified resource - :param resource: Azure resource endpoints. Default to Azure Resource Manager - Use 'az cloud show' command for other Azure resources - ''' - resource = (resource or cmd.cli_ctx.cloud.endpoints.active_directory_resource_id) - profile = Profile(cli_ctx=cmd.cli_ctx) - creds, subscription, tenant = profile.get_raw_token(subscription=None, resource=resource) - return { - 'tokenType': creds[0], - 'accessToken': creds[1], - 'expiresOn': creds[2].get('expiresOn', 'N/A'), - 'subscription': subscription, - 'tenant': tenant - } - - -def _parse_connection_string(cs, validate=None, cstring_type='entity'): +from azext_iot.common.auth import get_aad_token + + +def _parse_connection_string(cs, validate=None, cstring_type="entity"): decomposed = validate_key_value_pairs(cs) decomposed_lower = dict((k.lower(), v) for k, v in decomposed.items()) if validate: for k in validate: if not any([decomposed.get(k), decomposed_lower.get(k.lower())]): - raise ValueError('{} connection string has missing property: {}'.format(cstring_type, k)) + raise ValueError( + "{} connection string has missing property: {}".format( + cstring_type, k + ) + ) return decomposed def parse_pnp_connection_string(cs): - validate = ['HostName', 'RepositoryId', 'SharedAccessKeyName', 'SharedAccessKey'] - return _parse_connection_string(cs, validate, 'PnP Model Repository') + validate = ["HostName", "RepositoryId", "SharedAccessKeyName", "SharedAccessKey"] + return _parse_connection_string(cs, validate, "PnP Model Repository") def parse_iot_hub_connection_string(cs): - validate = ['HostName', 'SharedAccessKeyName', 'SharedAccessKey'] - return _parse_connection_string(cs, validate, 'IoT Hub') + validate = ["HostName", "SharedAccessKeyName", "SharedAccessKey"] + return _parse_connection_string(cs, validate, "IoT Hub") def parse_iot_device_connection_string(cs): - validate = ['HostName', 'DeviceId', 'SharedAccessKey'] - return _parse_connection_string(cs, validate, 'Device') + validate = ["HostName", "DeviceId", "SharedAccessKey"] + return _parse_connection_string(cs, validate, "Device") def parse_iot_device_module_connection_string(cs): - validate = ['HostName', 'DeviceId', 'ModuleId', 'SharedAccessKey'] - return _parse_connection_string(cs, validate, 'Module') + validate = ["HostName", "DeviceId", "ModuleId", "SharedAccessKey"] + return _parse_connection_string(cs, validate, "Module") -CONN_STR_TEMPLATE = 'HostName={};SharedAccessKeyName={};SharedAccessKey={}' +CONN_STR_TEMPLATE = "HostName={};SharedAccessKeyName={};SharedAccessKey={}" def get_iot_hub_connection_string( - cmd, - hub_name, - resource_group_name, - policy_name='iothubowner', - key_type='primary', - include_events=False, - login=None): + cmd, + hub_name, + resource_group_name, + policy_name="iothubowner", + key_type="primary", + include_events=False, + login=None, +): """ Function used to build up dictionary of IoT Hub connection string parts @@ -101,27 +88,29 @@ def get_iot_hub_connection_string( raise CLIError(e) result = {} - result['cs'] = login - result['policy'] = decomposed_lower['sharedaccesskeyname'] - result['primarykey'] = decomposed_lower['sharedaccesskey'] - result['entity'] = decomposed_lower['hostname'] + result["cs"] = login + result["policy"] = decomposed_lower["sharedaccesskeyname"] + result["primarykey"] = decomposed_lower["sharedaccesskey"] + result["entity"] = decomposed_lower["hostname"] return result client = None - if getattr(cmd, 'cli_ctx', None): + if getattr(cmd, "cli_ctx", None): client = iot_hub_service_factory(cmd.cli_ctx) else: client = cmd def _find_iot_hub_from_list(hubs, hub_name): if hubs: - return next((hub for hub in hubs if hub_name.lower() == hub.name.lower()), None) + return next( + (hub for hub in hubs if hub_name.lower() == hub.name.lower()), None + ) return None if resource_group_name is None: hubs = client.list_by_subscription() if not hubs: - raise CLIError('No IoT Hub found in current subscription.') + raise CLIError("No IoT Hub found in current subscription.") target_hub = _find_iot_hub_from_list(hubs, hub_name) else: try: @@ -131,52 +120,66 @@ def _find_iot_hub_from_list(hubs, hub_name): if target_hub is None: raise CLIError( - 'No IoT Hub found with name {} in current subscription.'.format(hub_name)) + "No IoT Hub found with name {} in current subscription.".format(hub_name) + ) try: - addprops = getattr(target_hub, 'additional_properties', None) - resource_group_name = addprops.get('resourcegroup') if addprops else getattr( - target_hub, 'resourcegroup', None) + addprops = getattr(target_hub, "additional_properties", None) + resource_group_name = ( + addprops.get("resourcegroup") + if addprops + else getattr(target_hub, "resourcegroup", None) + ) - policy = client.get_keys_for_key_name(resource_group_name, target_hub.name, policy_name) + policy = client.get_keys_for_key_name( + resource_group_name, target_hub.name, policy_name + ) except Exception: pass if policy is None: raise CLIError( - 'No keys found for policy {} of IoT Hub {}.'.format(policy_name, hub_name) + "No keys found for policy {} of IoT Hub {}.".format(policy_name, hub_name) ) - result['cs'] = CONN_STR_TEMPLATE.format( + result["cs"] = CONN_STR_TEMPLATE.format( target_hub.properties.host_name, policy.key_name, - policy.primary_key if key_type == 'primary' else policy.secondary_key) - result['entity'] = target_hub.properties.host_name - result['policy'] = policy_name - result['primarykey'] = policy.primary_key - result['secondarykey'] = policy.secondary_key - result['subscription'] = client.config.subscription_id - result['resourcegroup'] = resource_group_name - result['location'] = target_hub.location - result['sku_tier'] = target_hub.sku.tier.value + policy.primary_key if key_type == "primary" else policy.secondary_key, + ) + result["entity"] = target_hub.properties.host_name + result["policy"] = policy_name + result["primarykey"] = policy.primary_key + result["secondarykey"] = policy.secondary_key + result["subscription"] = client.config.subscription_id + result["resourcegroup"] = resource_group_name + result["location"] = target_hub.location + result["sku_tier"] = target_hub.sku.tier.value if include_events: events = {} - events['endpoint'] = trim_from_start(target_hub.properties.event_hub_endpoints['events'].endpoint, 'sb://').strip('/') - events['partition_count'] = target_hub.properties.event_hub_endpoints['events'].partition_count - events['path'] = target_hub.properties.event_hub_endpoints['events'].path - events['partition_ids'] = target_hub.properties.event_hub_endpoints['events'].partition_ids - result['events'] = events + events["endpoint"] = trim_from_start( + target_hub.properties.event_hub_endpoints["events"].endpoint, "sb://" + ).strip("/") + events["partition_count"] = target_hub.properties.event_hub_endpoints[ + "events" + ].partition_count + events["path"] = target_hub.properties.event_hub_endpoints["events"].path + events["partition_ids"] = target_hub.properties.event_hub_endpoints[ + "events" + ].partition_ids + result["events"] = events return result def get_iot_dps_connection_string( - client, - dps_name, - resource_group_name, - policy_name='provisioningserviceowner', - key_type='primary'): + client, + dps_name, + resource_group_name, + policy_name="provisioningserviceowner", + key_type="primary", +): """ Function used to build up dictionary of IoT Hub Device Provisioning Service connection string parts @@ -199,7 +202,9 @@ def get_iot_dps_connection_string( def _find_iot_dps_from_list(all_dps, dps_name): if all_dps: - return next((dps for dps in all_dps if dps_name.lower() == dps.name.lower()), None) + return next( + (dps for dps in all_dps if dps_name.lower() == dps.name.lower()), None + ) return None try: @@ -209,66 +214,78 @@ def _find_iot_dps_from_list(all_dps, dps_name): if target_dps is None: raise CLIError( - 'No IoT Provisioning Service found ' - 'with name {} in current subscription.'.format(dps_name)) + "No IoT Provisioning Service found " + "with name {} in current subscription.".format(dps_name) + ) try: policy = client.iot_dps_resource.list_keys_for_key_name( - dps_name, - policy_name, - resource_group_name) + dps_name, policy_name, resource_group_name + ) except Exception: pass if policy is None: raise CLIError( - 'No keys found for policy {} of ' - 'IoT Provisioning Service {}.'.format(policy_name, dps_name) + "No keys found for policy {} of " + "IoT Provisioning Service {}.".format(policy_name, dps_name) ) result = {} - result['cs'] = CONN_STR_TEMPLATE.format( + result["cs"] = CONN_STR_TEMPLATE.format( target_dps.properties.service_operations_host_name, policy.key_name, - policy.primary_key if key_type == 'primary' else policy.secondary_key) - result['entity'] = target_dps.properties.service_operations_host_name - result['policy'] = policy_name - result['primarykey'] = policy.primary_key - result['secondarykey'] = policy.secondary_key - result['subscription'] = client.config.subscription_id + policy.primary_key if key_type == "primary" else policy.secondary_key, + ) + result["entity"] = target_dps.properties.service_operations_host_name + result["policy"] = policy_name + result["primarykey"] = policy.primary_key + result["secondarykey"] = policy.secondary_key + result["subscription"] = client.config.subscription_id return result def get_iot_central_tokens(cmd, app_id, central_api_uri): - def get_event_hub_token(app_id, iotcAccessToken): - import requests - url = "https://{}/v1-beta/applications/{}/diagnostics/sasTokens".format(central_api_uri, app_id) - response = requests.post(url, headers={'Authorization': 'Bearer {}'.format(iotcAccessToken)}) - return response.json() - - aad_token = _get_aad_token(cmd, resource="https://apps.azureiotcentral.com")['accessToken'] + import requests + + aad_token = get_aad_token(cmd, resource="https://apps.azureiotcentral.com")[ + "accessToken" + ] + + url = "https://{}.{}/system/iothubs/generateSasTokens".format( + app_id, central_api_uri + ) + response = requests.post( + url, headers={"Authorization": "Bearer {}".format(aad_token)} + ) + tokens = response.json() + + additional_help = ( + "Please ensure that the user is logged through the `az login` command, " + "has the correct tenant set (the users home tenant) and " + "has access to the application through http://apps.azureiotcentral.com" + ) + + if tokens.get("error"): + error_message = tokens["error"]["message"] + if tokens["error"]["code"].startswith("403.043.004."): + error_message = "{} {}".format(error_message, additional_help) - tokens = get_event_hub_token(app_id, aad_token) - - if tokens.get('error'): raise CLIError( - 'Error {} getting tokens. {}'.format(tokens['error']['code'], tokens['error']['message']) + "Error {} getting tokens. {}".format(tokens["error"]["code"], error_message) ) - return tokens - + if tokens.get("message"): + error_message = "{} {}".format(tokens["message"], additional_help) + raise CLIError(error_message) -def get_iot_hub_token_from_central_app_id(cmd, app_id, central_api_uri): - return get_iot_central_tokens(cmd, app_id, central_api_uri)['iothubTenantSasToken']['sasToken'] + return tokens def get_iot_pnp_connection_string( - cmd, - endpoint, - repo_id, - user_role='Admin', - login=None): + cmd, endpoint, repo_id, user_role="Admin", login=None +): """ Function used to build up dictionary of IoT PnP connection string parts @@ -286,7 +303,9 @@ def get_iot_pnp_connection_string( """ - from azure.cli.command_modules.iot.digitaltwinrepositoryprovisioningservice import DigitalTwinRepositoryProvisioningService + from azure.cli.command_modules.iot.digitaltwinrepositoryprovisioningservice import ( + DigitalTwinRepositoryProvisioningService, + ) from azure.cli.command_modules.iot._utils import get_auth_header from azext_iot.constants import PNP_REPO_ENDPOINT @@ -302,24 +321,31 @@ def get_iot_pnp_connection_string( raise CLIError(e) result = {} - result['cs'] = login - result['policy'] = decomposed['SharedAccessKeyName'] - result['primarykey'] = decomposed['SharedAccessKey'] - result['repository_id'] = decomposed['RepositoryId'] - result['entity'] = decomposed['HostName'] - result['entity'] = result['entity'].replace('https://', '') - result['entity'] = result['entity'].replace('http://', '') + result["cs"] = login + result["policy"] = decomposed["SharedAccessKeyName"] + result["primarykey"] = decomposed["SharedAccessKey"] + result["repository_id"] = decomposed["RepositoryId"] + result["entity"] = decomposed["HostName"] + result["entity"] = result["entity"].replace("https://", "") + result["entity"] = result["entity"].replace("http://", "") return result def _find_key_from_list(keys, user_role): if keys: - return next((key for key in keys if key.user_role.lower() == user_role.lower()), None) + return next( + (key for key in keys if key.user_role.lower() == user_role.lower()), + None, + ) return None if repo_id: client = DigitalTwinRepositoryProvisioningService(endpoint) headers = get_auth_header(cmd) - keys = client.get_keys_async(repository_id=repo_id, api_version=client.api_version, custom_headers=headers) + keys = client.get_keys_async( + repository_id=repo_id, + api_version=client.api_version, + custom_headers=headers, + ) if keys is None: raise CLIError('Auth key required for repository "{}"'.format(repo_id)) @@ -328,17 +354,19 @@ def _find_key_from_list(keys, user_role): if policy is None: raise CLIError( - 'No auth key found for repository "{}" with user_role "{}".'.format(repo_id, user_role) + 'No auth key found for repository "{}" with user_role "{}".'.format( + repo_id, user_role + ) ) - result['cs'] = policy.connection_string - result['entity'] = policy.service_endpoint - result['policy'] = policy.id - result['primarykey'] = policy.secret - result['repository_id'] = policy.repository_id + result["cs"] = policy.connection_string + result["entity"] = policy.service_endpoint + result["policy"] = policy.id + result["primarykey"] = policy.secret + result["repository_id"] = policy.repository_id else: - result['entity'] = PNP_REPO_ENDPOINT + result["entity"] = PNP_REPO_ENDPOINT - result['entity'] = result['entity'].replace('https://', '') - result['entity'] = result['entity'].replace('http://', '') + result["entity"] = result["entity"].replace("https://", "") + result["entity"] = result["entity"].replace("http://", "") return result diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 7b23688cb..63f1ae177 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -6,7 +6,6 @@ from knack.util import CLIError from azext_iot._factory import _bind_sdk -from azext_iot.common._azure import get_iot_hub_token_from_central_app_id from azext_iot.common.shared import SdkType from azext_iot.common.utility import unpack_msrest_error, init_monitoring from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication @@ -18,17 +17,28 @@ def find_between(s, start, end): def iot_central_device_show( - cmd, device_id, app_id, central_api_uri="api.azureiotcentral.com" + cmd, device_id, app_id, central_api_uri="azureiotcentral.com" ): - sasToken = get_iot_hub_token_from_central_app_id(cmd, app_id, central_api_uri) - endpoint = find_between(sasToken, "SharedAccessSignature sr=", "&sig=") - target = {"entity": endpoint} - auth = BasicSasTokenAuthentication(sas_token=sasToken) - service_sdk, errors = _bind_sdk(target, SdkType.service_sdk, auth=auth) - try: - return service_sdk.get_twin(device_id) - except errors.CloudError as e: - raise CLIError(unpack_msrest_error(e)) + from azext_iot.common._azure import get_iot_central_tokens + + tokens = get_iot_central_tokens(cmd, app_id, central_api_uri) + exception = None + + # The device could be in any hub associated with the given app. + # We must search through each IoT Hub until device is found. + for token_group in tokens.values(): + sas_token = token_group["iothubTenantSasToken"]["sasToken"] + endpoint = find_between(sas_token, "SharedAccessSignature sr=", "&sig=") + target = {"entity": endpoint} + auth = BasicSasTokenAuthentication(sas_token=sas_token) + service_sdk, errors = _bind_sdk(target, SdkType.service_sdk, auth=auth) + try: + return service_sdk.get_twin(device_id) + except errors.CloudError as e: + if exception is None: + exception = CLIError(unpack_msrest_error(e)) + + raise exception def iot_central_validate_messages( @@ -42,7 +52,7 @@ def iot_central_validate_messages( repair=False, properties=None, yes=False, - central_api_uri="api.azureiotcentral.com", + central_api_uri="azureiotcentral.com", ): provider = CentralDeviceProvider(cmd, app_id) _events3_runner( @@ -72,7 +82,7 @@ def iot_central_monitor_events( repair=False, properties=None, yes=False, - central_api_uri="api.azureiotcentral.com", + central_api_uri="azureiotcentral.com", ): _events3_runner( cmd=cmd, @@ -112,13 +122,16 @@ def _events3_runner( from azext_iot.operations.events3 import _builders, _events - eventHubTarget = _builders.EventTargetBuilder().build_central_event_hub_target( + event_hub_targets = _builders.EventTargetBuilder().build_central_event_hub_target( cmd, app_id, central_api_uri ) + executorTargets = [] - _events.executor( - eventHubTarget, - consumer_group=consumer_group, + for target in event_hub_targets: + executorTargets.append(_events.executorData(target, consumer_group)) + + _events.nExecutor( + executorTargets, enqueued_time=enqueued_time, properties=properties, timeout=timeout, diff --git a/azext_iot/operations/events3/_builders.py b/azext_iot/operations/events3/_builders.py index 4ea2daf33..a67527a13 100644 --- a/azext_iot/operations/events3/_builders.py +++ b/azext_iot/operations/events3/_builders.py @@ -2,56 +2,70 @@ import uamqp from azext_iot.common.sas_token_auth import SasTokenAuthentication -from azext_iot.common.utility import (parse_entity, unicode_binary_map, url_encode_str) +from azext_iot.common.utility import parse_entity, unicode_binary_map, url_encode_str # To provide amqp frame trace DEBUG = False -class AmqpBuilder(): +class AmqpBuilder: @classmethod def build_iothub_amqp_endpoint_from_target(cls, target, duration=360): - hub_name = target['entity'].split('.')[0] - user = "{}@sas.root.{}".format(target['policy'], hub_name) - sas_token = SasTokenAuthentication(target['entity'], target['policy'], - target['primarykey'], duration).generate_sas_token() - return url_encode_str(user) + ":{}@{}".format(url_encode_str(sas_token), target['entity']) + hub_name = target["entity"].split(".")[0] + user = "{}@sas.root.{}".format(target["policy"], hub_name) + sas_token = SasTokenAuthentication( + target["entity"], target["policy"], target["primarykey"], duration + ).generate_sas_token() + return url_encode_str(user) + ":{}@{}".format( + url_encode_str(sas_token), target["entity"] + ) -class EventTargetBuilder(): - +class EventTargetBuilder: def __init__(self): self.eventLoop = asyncio.new_event_loop() asyncio.set_event_loop(self.eventLoop) def build_iot_hub_target(self, target): - return self.eventLoop.run_until_complete(self._build_iot_hub_target_async(target)) + return self.eventLoop.run_until_complete( + self._build_iot_hub_target_async(target) + ) def build_central_event_hub_target(self, cmd, app_id, central_api_uri): - return self.eventLoop.run_until_complete(self._build_central_event_hub_target_async(cmd, app_id, central_api_uri)) + return self.eventLoop.run_until_complete( + self._build_central_event_hub_target_async(cmd, app_id, central_api_uri) + ) def _build_auth_container(self, target): - sas_uri = 'sb://{}/{}'.format(target['events']['endpoint'], target['events']['path']) - return uamqp.authentication.SASTokenAsync.from_shared_access_key(sas_uri, target['policy'], target['primarykey']) + sas_uri = "sb://{}/{}".format( + target["events"]["endpoint"], target["events"]["path"] + ) + return uamqp.authentication.SASTokenAsync.from_shared_access_key( + sas_uri, target["policy"], target["primarykey"] + ) def _build_auth_container_from_token(self, endpoint, path, token, tokenExpiry): - sas_uri = 'sb://{}/{}'.format(endpoint, path) - return uamqp.authentication.SASTokenAsync(audience=sas_uri, uri=sas_uri, expires_at=tokenExpiry, token=token) + sas_uri = "sb://{}/{}".format(endpoint, path) + return uamqp.authentication.SASTokenAsync( + audience=sas_uri, uri=sas_uri, expires_at=tokenExpiry, token=token + ) async def _query_meta_data(self, endpoint, path, auth): source = uamqp.address.Source(endpoint) - receive_client = uamqp.ReceiveClientAsync(source, auth=auth, timeout=30000, debug=DEBUG) + receive_client = uamqp.ReceiveClientAsync( + source, auth=auth, timeout=30000, debug=DEBUG + ) try: await receive_client.open_async() - message = uamqp.Message(application_properties={'name': path}) + message = uamqp.Message(application_properties={"name": path}) response = await receive_client.mgmt_request_async( message, - b'READ', - op_type=b'com.microsoft:eventhub', - status_code_field=b'status-code', - description_fields=b'status-description', - timeout=30000 + b"READ", + op_type=b"com.microsoft:eventhub", + status_code_field=b"status-code", + description_fields=b"status-description", + timeout=30000, ) test = response.get_data() return test @@ -59,8 +73,12 @@ async def _query_meta_data(self, endpoint, path, auth): await receive_client.close_async() async def _evaluate_redirect(self, endpoint): - source = uamqp.address.Source('amqps://{}/messages/events/$management'.format(endpoint)) - receive_client = uamqp.ReceiveClientAsync(source, timeout=30000, prefetch=1, debug=DEBUG) + source = uamqp.address.Source( + "amqps://{}/messages/events/$management".format(endpoint) + ) + receive_client = uamqp.ReceiveClientAsync( + source, timeout=30000, prefetch=1, debug=DEBUG + ) try: await receive_client.open_async() @@ -68,67 +86,85 @@ async def _evaluate_redirect(self, endpoint): except uamqp.errors.LinkRedirect as redirect: redirect = unicode_binary_map(parse_entity(redirect)) result = {} - result['events'] = {} - result['events']['endpoint'] = redirect['hostname'] - result['events']['path'] = redirect['address'].replace('amqps://', '').split('/')[1] - result['events']['address'] = redirect['address'] + result["events"] = {} + result["events"]["endpoint"] = redirect["hostname"] + result["events"]["path"] = ( + redirect["address"].replace("amqps://", "").split("/")[1] + ) + result["events"]["address"] = redirect["address"] return redirect, result finally: await receive_client.close_async() - async def _build_central_event_hub_target_async(self, cmd, app_id, central_api_uri): - from azext_iot.common._azure import get_iot_central_tokens - - tokens = get_iot_central_tokens(cmd, app_id, central_api_uri) - eventHubToken = tokens['eventhubSasToken'] - hostnameWithoutPrefix = eventHubToken['hostname'].split("/")[2] - endpoint = hostnameWithoutPrefix - path = eventHubToken["entityPath"] - tokenExpiry = tokens['expiry'] - auth = self._build_auth_container_from_token(endpoint, path, eventHubToken['sasToken'], tokenExpiry) - address = "amqps://{}/{}/$management".format(hostnameWithoutPrefix, eventHubToken["entityPath"]) + async def create_single_iotc_eventhub_target_async(self, tokens): + event_hub_token = tokens["eventhubSasToken"] + hostname_without_prefix = event_hub_token["hostname"].split("/")[2] + endpoint = hostname_without_prefix + path = event_hub_token["entityPath"] + token_expiry = tokens["expiry"] + auth = self._build_auth_container_from_token( + endpoint, path, event_hub_token["sasToken"], token_expiry + ) + address = "amqps://{}/{}/$management".format( + hostname_without_prefix, event_hub_token["entityPath"] + ) meta_data = await self._query_meta_data(address, path, auth) - partition_count = meta_data[b'partition_count'] + partition_count = meta_data[b"partition_count"] partition_ids = [] for i in range(int(partition_count)): partition_ids.append(str(i)) partitions = partition_ids - auth = self._build_auth_container_from_token(endpoint, path, eventHubToken['sasToken'], tokenExpiry) - - eventHubTarget = { - 'endpoint': endpoint, - 'path': path, - 'auth': auth, - 'partitions': partitions + auth = self._build_auth_container_from_token( + endpoint, path, event_hub_token["sasToken"], token_expiry + ) + + event_hub_target = { + "endpoint": endpoint, + "path": path, + "auth": auth, + "partitions": partitions, } - return eventHubTarget + return event_hub_target + + async def _build_central_event_hub_target_async(self, cmd, app_id, central_api_uri): + from azext_iot.common._azure import get_iot_central_tokens + + all_tokens = get_iot_central_tokens(cmd, app_id, central_api_uri) + targets = [ + await self.create_single_iotc_eventhub_target_async(tokens) + for tokens in all_tokens.values() + ] + + return targets async def _build_iot_hub_target_async(self, target): - if 'events' not in target: + if "events" not in target: endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target(target) _, update = await self._evaluate_redirect(endpoint) - target['events'] = update['events'] - endpoint = target['events']['endpoint'] - path = target['events']['path'] + target["events"] = update["events"] + endpoint = target["events"]["endpoint"] + path = target["events"]["path"] auth = self._build_auth_container(target) - meta_data = await self._query_meta_data(target['events']['address'], target['events']['path'], auth) - partition_count = meta_data[b'partition_count'] + meta_data = await self._query_meta_data( + target["events"]["address"], target["events"]["path"], auth + ) + partition_count = meta_data[b"partition_count"] partition_ids = [] for i in range(int(partition_count)): partition_ids.append(str(i)) - target['events']['partition_ids'] = partition_ids + target["events"]["partition_ids"] = partition_ids else: - endpoint = target['events']['endpoint'] - path = target['events']['path'] - partitions = target['events']['partition_ids'] + endpoint = target["events"]["endpoint"] + path = target["events"]["path"] + partitions = target["events"]["partition_ids"] auth = self._build_auth_container(target) eventHubTarget = { - 'endpoint': endpoint, - 'path': path, - 'auth': auth, - 'partitions': partitions + "endpoint": endpoint, + "path": path, + "auth": auth, + "partitions": partitions, } return eventHubTarget diff --git a/azext_iot/operations/events3/_events.py b/azext_iot/operations/events3/_events.py index 0df0a6d12..2bf63e701 100644 --- a/azext_iot/operations/events3/_events.py +++ b/azext_iot/operations/events3/_events.py @@ -24,6 +24,12 @@ logger = get_logger(__name__) +class executorData: + def __init__(self, target, consumer_group): + self.target = target + self.consumer_group = consumer_group + + def executor( target, consumer_group, @@ -40,26 +46,61 @@ def executor( simulate_errors=False, central_device_provider=None, ): + executor = executorData(target, consumer_group) + + return nExecutor( + [executor], + enqueued_time, + properties, + timeout, + device_id, + output, + content_type, + devices, + interface_name, + pnp_context, + validate_messages, + simulate_errors, + central_device_provider, + ) + +def nExecutor( + executorTargets, + enqueued_time, + properties=None, + timeout=0, + device_id=None, + output=None, + content_type=None, + devices=None, + interface_name=None, + pnp_context=None, + validate_messages=False, + simulate_errors=False, + central_device_provider=None, +): coroutines = [] - coroutines.append( - initiate_event_monitor( - target, - consumer_group, - enqueued_time, - device_id, - properties, - timeout, - output, - content_type, - devices, - interface_name, - pnp_context, - validate_messages, - simulate_errors, - central_device_provider, + + for executor in executorTargets: + coroutines.append( + initiate_event_monitor( + executor.target, + executor.consumer_group, + enqueued_time, + device_id, + properties, + timeout, + output, + content_type, + devices, + interface_name, + pnp_context, + validate_messages, + simulate_errors, + central_device_provider, + ) ) - ) loop = asyncio.get_event_loop() if loop.is_closed(): diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 407b5e739..ac88cdc73 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -48,6 +48,28 @@ def test_central_device_show(self): ) ) + # Verify incorrect app-id throws error + self.cmd( + 'iot central app device-twin show --app-id incorrect-app --device-id "{}"'.format( + DEVICE_ID + ), + expect_failure=True, + ) + # Verify incorrect device-id throws error + self.cmd( + 'iot central app device-twin show --app-id "{}" --device-id incorrect-device'.format( + APP_ID + ), + expect_failure=True, + ) + # Verify that no errors are thrown when device shown + # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices + self.cmd( + 'iot central app device-twin show --app-id "{}" --device-id "{}"'.format( + APP_ID, DEVICE_ID + ) + ) + def test_central_monitor_events(self): # Test with invalid app-id self.cmd( diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index 613d87e70..f35c00f9b 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -26,17 +26,6 @@ resource = "shared_resource" -@pytest.fixture() -def fixture_iot_token(mocker): - sas = mocker.patch( - "azext_iot.operations.central.get_iot_hub_token_from_central_app_id" - ) - sas.return_value = "SharedAccessSignature sr={}&sig=signature&se=expiry&skn=service".format( - resource - ) - return sas - - @pytest.fixture() def fixture_cmd(mocker): # Placeholder for later use @@ -91,45 +80,46 @@ def get(self, _value, _default): ] -@pytest.fixture() -def fixture_get_aad_token(mocker): - mock = mocker.patch("azext_iot.common._azure._get_aad_token") - mock.return_value = {"accessToken": "token"} - - @pytest.fixture() def fixture_get_iot_central_tokens(mocker): mock = mocker.patch("azext_iot.common._azure.get_iot_central_tokens") mock.return_value = { - "eventhubSasToken": { - "hostname": "part1/part2/part3", - "entityPath": "entityPath", - "sasToken": "sasToken", - }, - "expiry": "0000", - "iothubTenantSasToken": {"sasToken": "iothubTenantSasToken"}, + "id": { + "eventhubSasToken": { + "hostname": "part1/part2/part3", + "entityPath": "entityPath", + "sasToken": "sasToken", + }, + "expiry": "0000", + "iothubTenantSasToken": { + "sasToken": "SharedAccessSignature sr=shared_resource&sig=" + }, + } } class TestCentralHelpers: - def test_get_iot_central_tokens(self, fixture_requests_post, fixture_get_aad_token): + def test_get_iot_central_tokens(self, fixture_requests_post, fixture_azure_profile): from azext_iot.common._azure import get_iot_central_tokens + class Cmd: + cli_ctx = "" + # Test to ensure get_iot_central_tokens calls requests.post and tokens are returned assert ( - get_iot_central_tokens({}, "app_id", "api-uri").value() + get_iot_central_tokens(Cmd(), "app_id", "api-uri").value() == "fixture_requests_post value" ) def test_get_aad_token(self, fixture_azure_profile): - from azext_iot.common._azure import _get_aad_token + from azext_iot.common.auth import get_aad_token class Cmd: - cli_ctx = "test" + cli_ctx = "" # Test to ensure _get_aad_token is called and returns the right values based on profile.get_raw_tokens - assert _get_aad_token(Cmd(), "resource") == { + assert get_aad_token(Cmd(), "resource") == { "accessToken": "raw token 0 -b", "expiresOn": "value", "subscription": "raw token 1", @@ -137,25 +127,12 @@ class Cmd: "tokenType": "raw token 0 - A", } - def test_get_iot_hub_token_from_central_app_id( - self, fixture_get_iot_central_tokens - ): - from azext_iot.common._azure import get_iot_hub_token_from_central_app_id - - # Test to ensure get_iot_hub_token_from_central_app_id returns iothubTenantSasToken - assert ( - get_iot_hub_token_from_central_app_id({}, "app_id", "api-uri") - == "iothubTenantSasToken" - ) - class TestDeviceTwinShow: def test_device_twin_show_calls_get_twin( - self, fixture_iot_token, fixture_bind_sdk, fixture_cmd + self, fixture_bind_sdk, fixture_cmd, fixture_get_iot_central_tokens ): - result = subject.iot_central_device_show( - fixture_cmd, device_id, app_id, "api-uri" - ) + result = subject.iot_central_device_show(fixture_cmd, device_id, app_id) # Ensure get_twin is called and result is returned assert result is device_twin_result From acff680325f6b6dc0b81cdb0cb377aa64efffc91 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Wed, 29 Apr 2020 10:53:35 -0700 Subject: [PATCH 024/179] Validate messages now performs primitive type matching (#169) * Validate messages command now validates depth=1 field names * remove time module from IT, add user agent to dps * added help for central_dns_suffix * fixed lint issues, removed capability-model command * added linting guidance to CONTRIBUTING.md * Validate messages now performs primitive type matching * fixed lint issue, moved to using better parser * Move to using 'template' instead of 'DCM' --- azext_iot/common/utility.py | 134 ++++++++++----- azext_iot/operations/events3/_parser.py | 203 ++++++++++++++++------- azext_iot/tests/test_iot_utility_unit.py | 123 +++++++++++++- 3 files changed, 358 insertions(+), 102 deletions(-) diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index b60d58725..dafd84391 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -11,6 +11,7 @@ import ast import base64 +import isodate import json import os import sys @@ -34,13 +35,13 @@ def parse_entity(entity, filter_none=False): result (dict): a dictionary of attributes from the function input. """ result = {} - attributes = [attr for attr in dir(entity) if not attr.startswith('_')] + attributes = [attr for attr in dir(entity) if not attr.startswith("_")] for attribute in attributes: value = getattr(entity, attribute, None) if filter_none and not value: continue value_behavior = dir(value) - if '__call__' not in value_behavior: + if "__call__" not in value_behavior: result[attribute] = value return result @@ -73,19 +74,25 @@ def verify_transform(subject, mapping): verifies that subject[k] is of type mapping[k] """ import jmespath + for k in mapping.keys(): result = jmespath.search(k, subject) if result is None: raise AttributeError('The property "{}" is required'.format(k)) if not isinstance(result, mapping[k]): - supplemental_info = '' + supplemental_info = "" if mapping[k] == dict: - wiki_link = 'https://github.com/Azure/azure-iot-cli-extension/wiki/Tips' - supplemental_info = 'Review inline JSON examples here --> {}'.format(wiki_link) - - raise TypeError('The property "{}" must be of {} but is {}. Input: {}. {}'.format( - k, str(mapping[k]), str(type(result)), result, supplemental_info)) + wiki_link = "https://github.com/Azure/azure-iot-cli-extension/wiki/Tips" + supplemental_info = "Review inline JSON examples here --> {}".format( + wiki_link + ) + + raise TypeError( + 'The property "{}" must be of {} but is {}. Input: {}. {}'.format( + k, str(mapping[k]), str(type(result)), result, supplemental_info + ) + ) def validate_key_value_pairs(string): @@ -99,8 +106,8 @@ def validate_key_value_pairs(string): """ result = None if string: - kv_list = [x for x in string.split(';') if '=' in x] # key-value pairs - result = dict(x.split('=', 1) for x in kv_list) + kv_list = [x for x in string.split(";") if "=" in x] # key-value pairs + result = dict(x.split("=", 1) for x in kv_list) return result @@ -139,6 +146,7 @@ def shell_safe_json_parse(json_or_dict_string, preserve_order=False): if not preserve_order: return json.loads(json_or_dict_string) from collections import OrderedDict + return json.loads(json_or_dict_string, object_pairs_hook=OrderedDict) except ValueError as json_ex: try: @@ -146,14 +154,19 @@ def shell_safe_json_parse(json_or_dict_string, preserve_order=False): except SyntaxError: raise CLIError(json_ex) except ValueError as ex: - logger.debug(ex) # log the exception which could be a python dict parsing error. - raise CLIError(json_ex) # raise json_ex error which is more readable and likely. + logger.debug( + ex + ) # log the exception which could be a python dict parsing error. + raise CLIError( + json_ex + ) # raise json_ex error which is more readable and likely. def read_file_content(file_path, allow_binary=False): from codecs import open as codecs_open + # Note, always put 'utf-8-sig' first, so that BOM in WinOS won't cause trouble. - for encoding in ['utf-8-sig', 'utf-8', 'utf-16', 'utf-16le', 'utf-16be']: + for encoding in ["utf-8-sig", "utf-8", "utf-16", "utf-16le", "utf-16be"]: try: with codecs_open(file_path, encoding=encoding) as f: logger.debug("attempting to read file %s as %s", file_path, encoding) @@ -163,19 +176,19 @@ def read_file_content(file_path, allow_binary=False): if allow_binary: try: - with open(file_path, 'rb') as input_file: + with open(file_path, "rb") as input_file: logger.debug("attempting to read file %s as binary", file_path) return base64.b64encode(input_file.read()).decode("utf-8") except Exception: # pylint: disable=broad-except pass - raise CLIError('Failed to decode file {} - unknown decoding'.format(file_path)) + raise CLIError("Failed to decode file {} - unknown decoding".format(file_path)) def trim_from_start(s, substring): """ Trims a substring from the target string (if it exists) returning the trimmed string. Otherwise returns original target string. """ if s.startswith(substring): - s = s[len(substring):] + s = s[len(substring) :] return s @@ -186,12 +199,17 @@ def validate_min_python_version(major, minor, error_msg=None, exit_on_fail=True) if version.major > major: return True if major == version.major: - result = (version.minor >= minor) + result = version.minor >= minor if not result: if exit_on_fail: - msg = error_msg if error_msg else 'Python version {}.{} or higher required for this functionality.'.format( - major, minor) + msg = ( + error_msg + if error_msg + else "Python version {}.{} or higher required for this functionality.".format( + major, minor + ) + ) sys.exit(msg) return result @@ -205,10 +223,10 @@ def unicode_binary_map(target): for k in target: key = k if isinstance(k, bytes): - key = str(k, 'utf8') + key = str(k, "utf8") if isinstance(target[k], bytes): - result[key] = str(target[k], 'utf8') + result[key] = str(target[k], "utf8") else: result[key] = target[k] @@ -232,11 +250,11 @@ def execute_onthread(**kwargs): Event(), Thread(): Event object to set the cancellation flag, Executing Thread object """ - interval = kwargs.get('interval') - method = kwargs.get('method') - method_args = kwargs.get('args') - max_runs = kwargs.get('max_runs') - handle = kwargs.get('return_handle') + interval = kwargs.get("interval") + method = kwargs.get("method") + method_args = kwargs.get("args") + max_runs = kwargs.get("max_runs") + handle = kwargs.get("return_handle") if not interval: interval = 2 @@ -292,6 +310,7 @@ def url_encode_str(s, plus=False): def test_import(package): """ Used to determine if a dependency is loading correctly """ import importlib + try: importlib.import_module(package) except ImportError: @@ -302,10 +321,10 @@ def test_import(package): def unpack_pnp_http_error(e): error = unpack_msrest_error(e) if isinstance(error, dict): - if error.get('error'): - error = error['error'] - if error.get('stackTrace'): - error.pop('stackTrace') + if error.get("error"): + error = error["error"] + if error.get("stackTrace"): + error.pop("stackTrace") return error @@ -340,13 +359,13 @@ def init_monitoring(cmd, timeout, properties, enqueued_time, repair, yes): validate_min_python_version(3, 5) if timeout < 0: - raise CLIError('Monitoring timeout must be 0 (inf) or greater.') - timeout = (timeout * 1000) + raise CLIError("Monitoring timeout must be 0 (inf) or greater.") + timeout = timeout * 1000 config = cmd.cli_ctx.config output = cmd.cli_ctx.invocation.data.get("output", None) if not output: - output = 'json' + output = "json" ensure_uamqp(config, yes, repair) if not properties: @@ -359,14 +378,19 @@ def init_monitoring(cmd, timeout, properties, enqueued_time, repair, yes): def get_sas_token(target): - from azext_iot.common.digitaltwin_sas_token_auth import DigitalTwinSasTokenAuthentication - token = '' - if target.get('repository_id'): - token = DigitalTwinSasTokenAuthentication(target["repository_id"], - target["entity"], - target["policy"], - target["primarykey"]).generate_sas_token() - return {'Authorization': '{}'.format(token)} + from azext_iot.common.digitaltwin_sas_token_auth import ( + DigitalTwinSasTokenAuthentication, + ) + + token = "" + if target.get("repository_id"): + token = DigitalTwinSasTokenAuthentication( + target["repository_id"], + target["entity"], + target["policy"], + target["primarykey"], + ).generate_sas_token() + return {"Authorization": "{}".format(token)} def dict_clean(d): @@ -396,7 +420,7 @@ def looks_like_file(element): ".java", ".ts", ".js", - ".cs" + ".cs", ) ) @@ -412,3 +436,29 @@ def ensure_pkg_resources_entries(): pkg_resources.working_set.add_entry(extension_path) return + + +class ISO8601Validator: + def is_iso8601_date(self, to_validate) -> bool: + try: + return bool(isodate.parse_date(to_validate)) + except Exception: + return False + + def is_iso8601_datetime(self, to_validate: str) -> bool: + try: + return bool(isodate.parse_datetime(to_validate)) + except Exception: + return False + + def is_iso8601_duration(self, to_validate: str) -> bool: + try: + return bool(isodate.parse_duration(to_validate)) + except Exception: + return False + + def is_iso8601_time(self, to_validate: str) -> bool: + try: + return bool(isodate.parse_time(to_validate)) + except Exception: + return False diff --git a/azext_iot/operations/events3/_parser.py b/azext_iot/operations/events3/_parser.py index 22d5a12e5..c01d845b0 100644 --- a/azext_iot/operations/events3/_parser.py +++ b/azext_iot/operations/events3/_parser.py @@ -4,13 +4,14 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import json import random import re -import json from knack.log import get_logger from uamqp.message import Message -from azext_iot.common.utility import parse_entity, unicode_binary_map + +from azext_iot.common.utility import parse_entity, unicode_binary_map, ISO8601Validator from azext_iot.central.providers import CentralDeviceProvider SUPPORTED_ENCODINGS = ["utf-8"] @@ -18,6 +19,8 @@ INTERFACE_NAME_IDENTIFIER = b"iothub-interface-name" random.seed(0) +ios_validator = ISO8601Validator() + class Event3Parser(object): _logger = get_logger(__name__) @@ -97,14 +100,16 @@ def parse_message( message, origin_device_id, content_type, create_payload_error ) - self._validate_payload_against_dcm( - origin_device_id, - payload, - central_device_provider, - create_payload_name_error, + self._perform_static_validations( + origin_device_id=origin_device_id, payload=payload ) - self._validate_field_names(origin_device_id, payload) + self._perform_dynamic_validations( + origin_device_id=origin_device_id, + payload=payload, + central_device_provider=central_device_provider, + create_payload_name_error=create_payload_name_error, + ) event["payload"] = payload @@ -163,9 +168,7 @@ def _parse_system_properties(self, message: Message): return unicode_binary_map(parse_entity(message.properties, True)) except Exception: self._errors.append( - "Failed to parse system_properties for message {message}.".format( - message - ) + "Failed to parse system_properties for message {}.".format(message) ) return {} @@ -270,83 +273,165 @@ def _parse_payload( return payload - def _validate_payload_against_dcm( + # Static validations should only need information present in the payload + # i.e. there should be no need for network calls + def _perform_static_validations(self, origin_device_id: str, payload: dict): + # if its not a dictionary, something else went wrong with parsing + if not isinstance(payload, dict): + return + + self._validate_field_names(origin_device_id=origin_device_id, payload=payload) + + def _validate_field_names(self, origin_device_id: str, payload: dict): + # source: + # https://github.com/Azure/IoTPlugandPlay/tree/master/DTDL + regex = "^[a-zA-Z_][a-zA-Z0-9_]*$" + + # if a field name does not match the above regex, it is an invalid field name + invalid_field_names = [ + field_name + for field_name in payload.keys() + if not re.search(regex, field_name) + ] + if invalid_field_names: + self._errors.append( + "The following field names are not allowed: '{}'. " + "Payload: '{}'. " + "Message origin: '{}'.".format( + invalid_field_names, payload, origin_device_id + ) + ) + + # Dynamic validations should need data external to the payload + # e.g. device template + def _perform_dynamic_validations( self, origin_device_id: str, - payload: str, + payload: dict, central_device_provider: CentralDeviceProvider, create_payload_name_error=False, ): - if not central_device_provider: + # if the payload is not a dictionary some other parsing error occurred + if not isinstance(payload, dict): + return + + # device provider was not passed in, no way to get the device template + if not isinstance(central_device_provider, CentralDeviceProvider): + return + + template = self._get_device_template( + origin_device_id=origin_device_id, + central_device_provider=central_device_provider, + ) + + # _get_device_template should log error if there was an issue + if not template: return - if not hasattr(payload, "keys"): - # some error happend while parsing - # should be captured by _parse_payload method above + template_schemas = self._extract_template_schemas_from_template( + origin_device_id=origin_device_id, template=template + ) + + # _extract_template_schemas_from_template should log error if there was an issue + if not isinstance(template_schemas, dict): return + self._validate_payload_against_schema( + origin_device_id=origin_device_id, + payload=payload, + template_schemas=template_schemas, + ) + + def _get_device_template( + self, origin_device_id: str, central_device_provider: CentralDeviceProvider, + ): try: - template = central_device_provider.get_device_template_by_device_id( + return central_device_provider.get_device_template_by_device_id( origin_device_id ) except Exception as e: self._errors.append( - "Unable to get DCM for device: {}." + "Unable to retrieve template for device: {}." "Inner exception: {}".format(origin_device_id, e) ) - return + def _extract_template_schemas_from_template( + self, origin_device_id: str, template: dict + ): try: - all_schema = self._extract_schema_from_template(template) - all_names = [schema["name"] for schema in all_schema] + schemas = [] + dcm = template["capabilityModel"] + implements = dcm["implements"] + for implementation in implements: + contents = implementation["schema"]["contents"] + schemas.extend(contents) + return {schema["name"]: schema for schema in schemas} except Exception: self._errors.append( "Unable to extract device schema for device: {}." "Template: {}".format(origin_device_id, template) ) - return - for telemetry_name in payload.keys(): - if create_payload_name_error or telemetry_name not in all_names: + # 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_against_schema( + self, origin_device_id: str, payload: dict, template_schemas: dict, + ): + template_schema_names = template_schemas.keys() + for name, value in payload.items(): + schema = template_schemas.get(name) + if not schema: self._errors.append( - "Telemetry item '{}' is not present in DCM. " + "Telemetry item '{}' is not present in capability model. " "Device ID: {}. " "List of allowed telemetry values for this type of device: {}. " "NOTE: telemetry names are CASE-SENSITIVE".format( - telemetry_name, origin_device_id, all_names + name, origin_device_id, template_schema_names ) ) - def _extract_schema_from_template(self, template): - all_schema = [] - dcm = template["capabilityModel"] - implements = dcm["implements"] - for implementation in implements: - contents = implementation["schema"]["contents"] - all_schema.extend(contents) - - return all_schema - - def _validate_field_names(self, origin_device_id: str, payload: dict): - # if its not a dictionary, something else went wrong with parsing - if not isinstance(payload, dict): - return - - # source: - # https://github.com/Azure/IoTPlugandPlay/tree/master/DTDL - regex = "^[a-zA-Z_][a-zA-Z0-9_]*$" - - # if a field name does not match the above regex, it is an invalid field name - invalid_field_names = [ - field_name - for field_name in payload.keys() - if not re.search(regex, field_name) - ] - if invalid_field_names: - self._errors.append( - "The following field names are not allowed: '{}'. " - "Payload: '{}'. " - "Message origin: '{}'.".format( - invalid_field_names, payload, origin_device_id + is_dict = isinstance(schema, dict) + if is_dict and not self._validate_types_match(value, schema): + expected_type = str(schema.get("schema")) + self._errors.append( + "Type mismatch. " + "Expected type: '{}'. " + "Value received: '{}'. " + "Telemetry identifier: {}. " + "Device ID: {}. " + "All dates/times/datetimes/durations must be ISO 8601 compliant.".format( + expected_type, value, name, origin_device_id + ) ) - ) + + def _validate_types_match(self, value, schema: dict) -> bool: + # suppress error if there is no "schema" in schema + # means something else went wrong + schema_type = schema.get("schema") + if not schema_type: + return True + + if schema_type == "boolean": + return isinstance(value, bool) + elif schema_type == "double": + return isinstance(value, (float, int)) + elif schema_type == "float": + return isinstance(value, (float, int)) + elif schema_type == "integer": + return isinstance(value, int) + elif schema_type == "long": + return isinstance(value, (float, int)) + elif schema_type == "string": + return isinstance(value, str) + elif schema_type == "time": + return ios_validator.is_iso8601_time(value) + elif schema_type == "date": + return ios_validator.is_iso8601_date(value) + elif schema_type == "dateTime": + return ios_validator.is_iso8601_datetime(value) + elif schema_type == "duration": + return ios_validator.is_iso8601_duration(value) + + # if the schema_type is not found above, suppress error + return True diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index 5333b6447..46f2d4f4c 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -17,6 +17,7 @@ process_json_arg, read_file_content, logger, + ISO8601Validator, ) from azext_iot.common.deps import ensure_uamqp from azext_iot.constants import EVENT_LIB, EXTENSION_NAME @@ -284,6 +285,73 @@ def test_file_json_fail_invalidcontent(self, content, argname, set_cwd, mocker): assert mocked_util_logger.call_count == 0 +# none of these are valid anything in ISO 8601 +BAD_ARRAY = ["asd", "", 123.4, 123, True, False] + + +class TestISO8601Validator: + validator = ISO8601Validator() + + # Success suite + @pytest.mark.parametrize( + "to_validate", ["20200101", "20200101Z", "2020-01-01", "2020-01-01Z"] + ) + def test_is_iso8601_date_pass(self, to_validate): + result = self.validator.is_iso8601_date(to_validate) + assert result + + @pytest.mark.parametrize( + "to_validate", + [ + "20200101T00:00:00", + "20200101T000000", + "2020-01-01T00:00:00", + "2020-01-01T00:00:00Z", + "2020-01-01T00:00:00.00", + "2020-01-01T00:00:00.00Z", + "2020-01-01T00:00:00.00+08:30", + ], + ) + def test_is_iso8601_datetime_pass(self, to_validate): + result = self.validator.is_iso8601_datetime(to_validate) + assert result + + @pytest.mark.parametrize("to_validate", ["P32DT7.592380349524318S", "P32DT7S"]) + def test_is_iso8601_duration_pass(self, to_validate): + result = self.validator.is_iso8601_duration(to_validate) + assert result + + @pytest.mark.parametrize( + "to_validate", ["00:00:00+08:30", "00:00:00Z", "00:00:00.123Z"] + ) + def test_is_iso8601_time_pass(self, to_validate): + result = self.validator.is_iso8601_time(to_validate) + assert result + + # Failure suite + @pytest.mark.parametrize( + "to_validate", ["2020-13-35", *BAD_ARRAY], + ) + def test_is_iso8601_date_fail(self, to_validate): + result = self.validator.is_iso8601_date(to_validate) + assert not result + + @pytest.mark.parametrize("to_validate", ["2020-13-35", "2020-00-00T", *BAD_ARRAY]) + def test_is_iso8601_datetime_fail(self, to_validate): + result = self.validator.is_iso8601_datetime(to_validate) + assert not result + + @pytest.mark.parametrize("to_validate", ["2020-01", *BAD_ARRAY]) + def test_is_iso8601_duration_fail(self, to_validate): + result = self.validator.is_iso8601_duration(to_validate) + assert not result + + @pytest.mark.parametrize("to_validate", [*BAD_ARRAY]) + def test_is_iso8601_time_fail(self, to_validate): + result = self.validator.is_iso8601_time(to_validate) + assert not result + + class TestEvents3Parser: device_id = "some-device-id" payload = {"String": "someValue"} @@ -296,6 +364,7 @@ class TestEvents3Parser: bad_content_type = "bad-content-type" bad_dcm_payload = {"temperature": "someValue"} + type_mismatch_payload = {"Bool": "someValue"} def test_parse_message_should_succeed(self): # setup @@ -614,7 +683,7 @@ def test_validate_against_template_should_fail(self): assert len(parser._info) == 0 actual_error = parser._errors[0] - expected_error = "Telemetry item '{}' is not present in DCM.".format( + expected_error = "Telemetry item '{}' is not present in capability model.".format( list(self.bad_dcm_payload)[0] ) assert expected_error in actual_error @@ -660,3 +729,55 @@ def test_validate_against_bad_template_should_not_throw(self): actual_error = parser._errors[0] assert "Unable to extract device schema for device" in actual_error + + def test_type_mismatch_should_error(self): + # setup + app_prop_type = "some_property" + app_prop_value = "some_value" + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.type_mismatch_payload).encode(), + properties=properties, + annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + application_properties={app_prop_type.encode(): app_prop_value.encode()}, + ) + parser = _parser.Event3Parser() + + provider = CentralDeviceProvider(cmd=None, app_id=None) + device_template = load_json(FileNames.central_device_template_file) + provider.get_device_template_by_device_id = mock.MagicMock( + return_value=device_template + ) + + # act + parsed_msg = parser.parse_message( + message=message, + pnp_context=False, + interface_name=None, + properties={"all"}, + content_type_hint=None, + simulate_errors=False, + central_device_provider=provider, + ) + + # verify + assert parsed_msg["event"]["payload"] == self.type_mismatch_payload + assert parsed_msg["event"]["origin"] == self.device_id + + assert len(parser._errors) == 1 + assert len(parser._warnings) == 0 + assert len(parser._info) == 0 + + actual_error = parser._errors[0] + assert "Type mismatch" in actual_error + assert "Type mismatch" in actual_error + assert "Value received" in actual_error + assert "Device ID" in actual_error + assert ( + "All dates/times/datetimes/durations must be ISO 8601 compliant." + in actual_error + ) + assert list(self.type_mismatch_payload.values())[0] in actual_error + assert list(self.type_mismatch_payload.keys())[0] in actual_error From 77a88eee98868abae49f43f3a7494e1498f7314d Mon Sep 17 00:00:00 2001 From: valluriraj Date: Thu, 30 Apr 2020 21:07:49 -0700 Subject: [PATCH 025/179] Add provisioning filters (#171) * first cut filtering options * additional filtering options * add __init_.py * address additional review comments * lint check updates * add integration tests Co-authored-by: Raj valluri --- azext_iot/central/commands_device.py | 12 ++- azext_iot/central/models/__init__.py | 5 ++ azext_iot/central/models/enum.py | 23 ++++++ azext_iot/central/params.py | 9 ++- .../central/providers/device_provider.py | 78 +++++++++++++++---- azext_iot/central/services/device.py | 36 +++++++++ azext_iot/tests/test_iot_central_int.py | 38 ++++++++- 7 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 azext_iot/central/models/__init__.py create mode 100644 azext_iot/central/models/enum.py diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py index 5933bbb3f..2dad7c2cc 100644 --- a/azext_iot/central/commands_device.py +++ b/azext_iot/central/commands_device.py @@ -68,10 +68,16 @@ def registration_info( device_id=None, token=None, central_dns_suffix="azureiotcentral.com", + device_status=None, ): - provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) + provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token,) if not device_id: - return provider.get_all_registration_info(central_dns_suffix=central_dns_suffix) + return provider.get_all_registration_info( + central_dns_suffix=central_dns_suffix, device_status=device_status + ) + return provider.get_device_registration_info( - device_id=device_id, central_dns_suffix=central_dns_suffix + device_id=device_id, + central_dns_suffix=central_dns_suffix, + device_status=device_status, ) diff --git a/azext_iot/central/models/__init__.py b/azext_iot/central/models/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/central/models/__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/central/models/enum.py b/azext_iot/central/models/enum.py new file mode 100644 index 000000000..61dd1c345 --- /dev/null +++ b/azext_iot/central/models/enum.py @@ -0,0 +1,23 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +""" +Enum definitions for central + +""" + +from enum import Enum + + +class DeviceStatus(Enum): + """ + Type of Device status. + """ + + provisioned = "provisioned" + registered = "registered" + blocked = "blocked" + unassociated = "unassociated" diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index b9f8aa1af..e2e3d3da5 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -8,7 +8,8 @@ CLI parameter definitions. """ -from azure.cli.core.commands.parameters import get_three_state_flag +from azure.cli.core.commands.parameters import get_three_state_flag, get_enum_type +from azext_iot.central.models.enum import DeviceStatus def load_central_arguments(self, _): @@ -60,3 +61,9 @@ def load_central_arguments(self, _): help="Central dns suffix. " "This enables running cli commands against non public/prod environments", ) + context.argument( + "device_status", + options_list=["--devicestatus", "--ds"], + arg_type=get_enum_type(DeviceStatus), + help="Indicates filter option for device status", + ) diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 505574aa9..47a51838d 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -8,6 +8,7 @@ from knack.log import get_logger from azext_iot.central import services as central_services from azext_iot.dps.services import global_service as dps_global_service +from azext_iot.central.models.enum import DeviceStatus logger = get_logger(__name__) @@ -170,44 +171,91 @@ def get_device_credentials( return credentials def get_device_registration_info( - self, device_id, central_dns_suffix="azureiotcentral.com", + self, + device_id, + device_status: DeviceStatus, + central_dns_suffix="azureiotcentral.com", ): + dps_state = {} info = self._device_registration_info.get(device_id) if not info: - credentials = self.get_device_credentials( - device_id=device_id, central_dns_suffix=central_dns_suffix + device_info = self.get_device(device_id) + device_essential_info = central_services.device.device_populate_essential_info( + device_info, device_status ) - id_scope = credentials["idScope"] - key = credentials["symmetricKey"]["primaryKey"] - dps_state = dps_global_service.get_registration_state( - id_scope=id_scope, key=key, device_id=device_id + if ( + device_essential_info.get("deviceStatus") + is DeviceStatus.provisioned.value + ): + credentials = self.get_device_credentials( + device_id=device_id, central_dns_suffix=central_dns_suffix + ) + id_scope = credentials["idScope"] + key = credentials["symmetricKey"]["primaryKey"] + dps_state = dps_global_service.get_registration_state( + id_scope=id_scope, key=key, device_id=device_id + ) + dps_state = self.dps_populate_essential_info( + dps_state, device_essential_info.get("deviceStatus") ) - central_info = self.get_device(device_id) info = { "@device_id": device_id, "dps_state": dps_state, - "central_info": central_info, + "device_info": device_essential_info, } self._device_registration_info[device_id] = info return info - def get_all_registration_info(self, central_dns_suffix="azureiotcentral.com"): + def dps_populate_essential_info(self, dps_info, device_status): + error = { + "provisioned": "None", + "registered": "Device it not yet provisioned.", + "blocked": "Device is blocked by admin", + "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 + + def get_all_registration_info( + self, device_status, central_dns_suffix="azureiotcentral.com" + ): + logger.warning("This command may take a long time to complete execution.") devices = self.list_devices(central_dns_suffix=central_dns_suffix) + real_devices = [ device for device in devices.values() if not device["simulated"] ] - if len(devices) != len(real_devices): + + real_devices_with_status = [ + central_services.device.update_device_status(device) + for device in real_devices + ] + + filtered_devices = real_devices_with_status + + if device_status: + filtered_devices = [ + device + for device in real_devices_with_status + if device.get("deviceStatus") == device_status + ] + + if len(devices) != len(filtered_devices): logger.warning( - "Getting registration info for following devices. " - "All other devices are simulated. " - "{}".format([device["id"] for device in real_devices]) + "Getting registration info for real devices. " + "{}".format([device["id"] for device in filtered_devices]) ) result = [ - self.get_device_registration_info(device["id"]) for device in real_devices + self.get_device_registration_info(device["id"], device["deviceStatus"]) + for device in filtered_devices ] return result diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index 717eb89c7..03859e2ad 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -9,6 +9,7 @@ from knack.util import CLIError from azext_iot.central.services import _utility +from azext_iot.central.models.enum import DeviceStatus BASE_PATH = "api/preview/devices" @@ -175,3 +176,38 @@ def get_device_credentials( response = requests.get(url, headers=headers) return _utility.try_extract_result(response) + + +def determine_device_status(device): + if device["approved"] is False: + return DeviceStatus.blocked.value + else: + if not device.get("instanceOf"): + return DeviceStatus.unassociated.value + + else: + if device["provisioned"] is False: + return DeviceStatus.registered.value + + else: + return DeviceStatus.provisioned.value + + +def update_device_status(device): + updated_device = device_populate_essential_info( + device, determine_device_status(device) + ) + return updated_device + + +def device_populate_essential_info(device, value): + if not value: + return update_device_status(device) + updated_device_data = { + "id": device["id"], + "displayName": device.get("displayName"), + "instanceOf": device.get("instanceOf"), + "simulated": device.get("simulated"), + "deviceStatus": value, + } + return updated_device_data diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index ac88cdc73..114346b75 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -204,5 +204,41 @@ def test_central_device_registration_info(self): # since time taken for provisioning to complete is not known # we can only assert that the payload is populated, not anything specific beyond that - assert json_result["central_info"] is not None + assert json_result["device_info"] is not None assert json_result["dps_state"] is not None + + def test_central_device_registration_info_filter(self): + device_id = self.create_random_name(prefix="aztest", length=24) + device_name = self.create_random_name(prefix="aztest", length=24) + device_status_expected = "unassociated" + + self.cmd( + "iot central app device create --app-id {} -d {} --device-name {}".format( + APP_ID, device_id, device_name + ), + checks=[ + self.check("approved", True), + self.check("displayName", device_name), + self.check("id", device_id), + self.check("simulated", False), + ], + ) + + result = self.cmd( + "iot central app device registration-info --app-id {} --ds {}".format( + APP_ID, device_status_expected + ) + ) + + self.cmd( + "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), + checks=[self.check("result", "success")], + ) + json_result = [] + device_info_results = [] + json_result = result.get_output_in_json() + for device in json_result: + device_info_results.append(device.get("device_info")) + + for device in device_info_results: + assert device.get("deviceStatus") == device_status_expected From ec60169882b97b24c23da6c8b670744a8b9a7501 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Fri, 1 May 2020 19:38:59 -0700 Subject: [PATCH 026/179] Added integration test for central device in registered state (#177) * Added integration test for central device in registered state --- azext_iot/central/models/device.py | 33 +++++++ .../central/providers/device_provider.py | 87 ++++++++----------- azext_iot/central/services/device.py | 71 +++++---------- azext_iot/tests/test_iot_central_int.py | 62 ++++++++++++- 4 files changed, 155 insertions(+), 98 deletions(-) create mode 100644 azext_iot/central/models/device.py diff --git a/azext_iot/central/models/device.py b/azext_iot/central/models/device.py new file mode 100644 index 000000000..b30670bf5 --- /dev/null +++ b/azext_iot/central/models/device.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.central.models.enum import DeviceStatus + + +class Device: + 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") + self.instance_of = device.get("instanceOf") + self.provisioned = device.get("provisioned") + self.simulated = device.get("simulated") + self.device_status = self._parse_device_status() + pass + + def _parse_device_status(self) -> DeviceStatus: + if not self.approved: + return DeviceStatus.blocked + + if not self.instance_of: + return DeviceStatus.unassociated + + if not self.provisioned: + return DeviceStatus.registered + + return DeviceStatus.provisioned diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 47a51838d..58eea5805 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -6,9 +6,11 @@ from knack.util import CLIError from knack.log import get_logger +from typing import List from azext_iot.central import services as central_services -from azext_iot.dps.services import global_service as dps_global_service from azext_iot.central.models.enum import DeviceStatus +from azext_iot.central.models.device import Device +from azext_iot.dps.services import global_service as dps_global_service logger = get_logger(__name__) @@ -36,7 +38,7 @@ def __init__(self, cmd, app_id: str, token=None): def get_device( self, device_id, central_dns_suffix="azureiotcentral.com", - ): + ) -> Device: if not device_id: raise CLIError("Device id must be specified.") # get or add to cache @@ -65,8 +67,7 @@ def get_device_template_by_device_id( raise CLIError("Device id must be specified.") device = self.get_device(device_id, central_dns_suffix) - instance_of = device.get("instanceOf") - if not instance_of: + if not device.instance_of: raise CLIError( "Device '{}' does not have a corresponding device template.".format( device_id @@ -75,20 +76,18 @@ def get_device_template_by_device_id( template = CentralDeviceTemplateProvider.get_device_template( self=self, - device_template_id=instance_of, + device_template_id=device.instance_of, central_dns_suffix=central_dns_suffix, ) return template - def list_devices( - self, central_dns_suffix="azureiotcentral.com", - ): + def list_devices(self, central_dns_suffix="azureiotcentral.com") -> List[Device]: devices = central_services.device.list_devices( cmd=self._cmd, app_id=self._app_id, token=self._token ) # add to cache - self._devices.update({device["id"]: device for device in devices}) + self._devices.update({device.id: device for device in devices}) return self._devices @@ -99,7 +98,7 @@ def create_device( instance_of=None, simulated=False, central_dns_suffix="azureiotcentral.com", - ): + ) -> Device: if not device_id: raise CLIError("Device id must be specified.") @@ -121,13 +120,13 @@ def create_device( raise CLIError("No device found with id: '{}'.".format(device_id)) # add to cache - self._devices[device["id"]] = device + self._devices[device.id] = device return device def delete_device( self, device_id, central_dns_suffix="azureiotcentral.com", - ): + ) -> dict: if not device_id: raise CLIError("Device id must be specified.") @@ -149,7 +148,7 @@ def delete_device( def get_device_credentials( self, device_id, central_dns_suffix="azureiotcentral.com", - ): + ) -> dict: credentials = self._device_credentials.get(device_id) if not credentials: @@ -175,35 +174,29 @@ def get_device_registration_info( device_id, device_status: DeviceStatus, central_dns_suffix="azureiotcentral.com", - ): + ) -> dict: dps_state = {} info = self._device_registration_info.get(device_id) - if not info: - device_info = self.get_device(device_id) - device_essential_info = central_services.device.device_populate_essential_info( - device_info, device_status + if info: + return info + + device = self.get_device(device_id) + if device.device_status == DeviceStatus.provisioned: + credentials = self.get_device_credentials( + device_id=device_id, central_dns_suffix=central_dns_suffix ) - if ( - device_essential_info.get("deviceStatus") - is DeviceStatus.provisioned.value - ): - credentials = self.get_device_credentials( - device_id=device_id, central_dns_suffix=central_dns_suffix - ) - id_scope = credentials["idScope"] - key = credentials["symmetricKey"]["primaryKey"] - dps_state = dps_global_service.get_registration_state( - id_scope=id_scope, key=key, device_id=device_id - ) - dps_state = self.dps_populate_essential_info( - dps_state, device_essential_info.get("deviceStatus") + id_scope = credentials["idScope"] + key = credentials["symmetricKey"]["primaryKey"] + dps_state = dps_global_service.get_registration_state( + id_scope=id_scope, key=key, device_id=device_id ) - info = { - "@device_id": device_id, - "dps_state": dps_state, - "device_info": device_essential_info, - } + dps_state = self.dps_populate_essential_info(dps_state, device.device_status) + info = { + "@device_id": device_id, + "dps_state": dps_state, + "device_info": device, + } self._device_registration_info[device_id] = info @@ -230,31 +223,25 @@ def get_all_registration_info( logger.warning("This command may take a long time to complete execution.") devices = self.list_devices(central_dns_suffix=central_dns_suffix) - real_devices = [ - device for device in devices.values() if not device["simulated"] - ] - - real_devices_with_status = [ - central_services.device.update_device_status(device) - for device in real_devices - ] + real_devices = [device for device in devices.values() if not device.simulated] - filtered_devices = real_devices_with_status + filtered_devices = real_devices if device_status: filtered_devices = [ device - for device in real_devices_with_status - if device.get("deviceStatus") == device_status + for device in real_devices + if device.device_status == device_status ] if len(devices) != len(filtered_devices): logger.warning( "Getting registration info for real devices. " - "{}".format([device["id"] for device in filtered_devices]) + "{}".format([device.id for device in filtered_devices]) ) + result = [ - self.get_device_registration_info(device["id"], device["deviceStatus"]) + self.get_device_registration_info(device.id, device.device_status) for device in filtered_devices ] diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index 03859e2ad..20af5c216 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -8,8 +8,10 @@ import requests from knack.util import CLIError +from typing import List + from azext_iot.central.services import _utility -from azext_iot.central.models.enum import DeviceStatus +from azext_iot.central.models.device import Device BASE_PATH = "api/preview/devices" @@ -20,7 +22,7 @@ def get_device( device_id: str, token: str, central_dns_suffix="azureiotcentral.com", -) -> dict: +) -> Device: """ Get device info given a device id @@ -40,12 +42,13 @@ def get_device( headers = _utility.get_headers(token, cmd) response = requests.get(url, headers=headers) - return _utility.try_extract_result(response) + result = _utility.try_extract_result(response) + return Device(result) def list_devices( - cmd, app_id: str, token: str, central_dns_suffix="azureiotcentral.com", -) -> list: + cmd, app_id: str, token: str, max_pages=1, central_dns_suffix="azureiotcentral.com", +) -> List[Device]: """ Get a list of all devices in IoTC app @@ -60,17 +63,25 @@ def list_devices( list of devices """ + devices = [] + url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) headers = _utility.get_headers(token, cmd) - response = requests.get(url, headers=headers) + pages_processed = 0 + while (pages_processed <= max_pages) and url: + response = requests.get(url, headers=headers) + result = _utility.try_extract_result(response) - 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 "value" not in result: - raise CLIError("Value is not present in body: {}".format(result)) + url = result.get("nextLink") + pages_processed = pages_processed + 1 - return result["value"] + return devices def create_device( @@ -82,7 +93,7 @@ def create_device( simulated: bool, token: str, central_dns_suffix="azureiotcentral.com", -) -> dict: +) -> Device: """ Create a device in IoTC @@ -116,7 +127,8 @@ def create_device( payload["instanceOf"] = instance_of response = requests.put(url, headers=headers, json=payload) - return _utility.try_extract_result(response) + result = _utility.try_extract_result(response) + return Device(result) def delete_device( @@ -176,38 +188,3 @@ def get_device_credentials( response = requests.get(url, headers=headers) return _utility.try_extract_result(response) - - -def determine_device_status(device): - if device["approved"] is False: - return DeviceStatus.blocked.value - else: - if not device.get("instanceOf"): - return DeviceStatus.unassociated.value - - else: - if device["provisioned"] is False: - return DeviceStatus.registered.value - - else: - return DeviceStatus.provisioned.value - - -def update_device_status(device): - updated_device = device_populate_essential_info( - device, determine_device_status(device) - ) - return updated_device - - -def device_populate_essential_info(device, value): - if not value: - return update_device_status(device) - updated_device_data = { - "id": device["id"], - "displayName": device.get("displayName"), - "instanceOf": device.get("instanceOf"), - "simulated": device.get("simulated"), - "deviceStatus": value, - } - return updated_device_data diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 114346b75..2fb0884b3 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -207,7 +207,7 @@ def test_central_device_registration_info(self): assert json_result["device_info"] is not None assert json_result["dps_state"] is not None - def test_central_device_registration_info_filter(self): + def test_central_device_registration_info_filter_unassociated(self): device_id = self.create_random_name(prefix="aztest", length=24) device_name = self.create_random_name(prefix="aztest", length=24) device_status_expected = "unassociated" @@ -242,3 +242,63 @@ def test_central_device_registration_info_filter(self): for device in device_info_results: assert device.get("deviceStatus") == device_status_expected + + def test_central_device_registration_info_filter_registered(self): + template = utility.process_json_arg( + DEVICE_TEMPLATE_PATH, argument_name="DEVICE_TEMPLATE_PATH" + ) + template_name = template["displayName"] + template_id = template_name + "id" + + device_id = self.create_random_name(prefix="aztest", length=24) + device_name = self.create_random_name(prefix="aztest", length=24) + device_status_expected = "registered" + + self.cmd( + "iot central app device-template create --app-id {} --device-template-id {} -k {}".format( + APP_ID, template_id, DEVICE_TEMPLATE_PATH + ), + checks=[ + self.check("displayName", template_name), + self.check("id", template_id), + ], + ) + + self.cmd( + "iot central app device create --app-id {} -d {} --device-name {} --instance-of {}".format( + APP_ID, device_id, device_name, template_id + ), + checks=[ + self.check("approved", True), + self.check("displayName", device_name), + self.check("id", device_id), + self.check("simulated", False), + ], + ) + + result = self.cmd( + "iot central app device registration-info --app-id {} --ds {}".format( + APP_ID, device_status_expected + ) + ) + + self.cmd( + "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), + checks=[self.check("result", "success")], + ) + + self.cmd( + "iot central app device-template delete --app-id {} --device-template-id {}".format( + APP_ID, template_id + ), + checks=[self.check("result", "success")], + ) + + json_result = [] + device_info_results = [] + json_result = result.get_output_in_json() + for device in json_result: + device_info_results.append(device.get("device_info")) + + for device in device_info_results: + assert device.get("deviceStatus") == device_status_expected From 83d05b04f920cc4fca714e12b9f71b14c48dcddc Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Fri, 1 May 2020 21:29:04 -0700 Subject: [PATCH 027/179] Cleaned up Central integration tests (#178) * Added integration test for central device in registered state * Refactored IT's --- azext_iot/tests/test_iot_central_int.py | 241 +++++++++++------------- 1 file changed, 109 insertions(+), 132 deletions(-) diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 2fb0884b3..4b3dc291f 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -5,19 +5,19 @@ # -------------------------------------------------------------------------------------------- import os +import time from azure.cli.testsdk import LiveScenarioTest from azext_iot.common import utility APP_ID = os.environ.get("azext_iot_central_app_id") -DEVICE_ID = os.environ.get("azext_iot_central_device_id") DEVICE_TEMPLATE_PATH = os.environ.get("azext_iot_central_device_template_path") -if not all([APP_ID, DEVICE_ID, DEVICE_TEMPLATE_PATH]): +if not all([APP_ID, DEVICE_TEMPLATE_PATH]): raise ValueError( - "Set azext_iot_central_app_id, azext_iot_central_device_id " - "and azext_iot_central_device_template_path to run central integration tests. " + "Set azext_iot_central_app_id, azext_iot_central_device_template_path" + "to run central integration tests. " ) @@ -25,51 +25,65 @@ class TestIotCentral(LiveScenarioTest): def __init__(self, test_case): super(TestIotCentral, self).__init__(test_case) - def test_central_device_show(self): + def test_central_device_twin_show_fail(self): + (device_id, _) = self._create_device() + # Verify incorrect app-id throws error self.cmd( - 'iotcentral device-twin show --app-id incorrect-app --device-id "{}"'.format( - DEVICE_ID + "iotcentral device-twin show --app-id incorrect-app --device-id {}".format( + device_id ), expect_failure=True, ) # Verify incorrect device-id throws error self.cmd( - 'iotcentral device-twin show --app-id "{}" --device-id incorrect-device'.format( + "iotcentral device-twin show --app-id {} --device-id incorrect-device".format( APP_ID ), expect_failure=True, ) - # Verify that no errors are thrown when device shown - # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices - self.cmd( - 'iotcentral device-twin show --app-id "{}" --device-id "{}"'.format( - APP_ID, DEVICE_ID - ) - ) # Verify incorrect app-id throws error self.cmd( - 'iot central app device-twin show --app-id incorrect-app --device-id "{}"'.format( - DEVICE_ID + "iot central app device-twin show --app-id incorrect-app --device-id {}".format( + device_id ), expect_failure=True, ) # Verify incorrect device-id throws error self.cmd( - 'iot central app device-twin show --app-id "{}" --device-id incorrect-device'.format( + "iot central app device-twin show --app-id {} --device-id incorrect-device".format( APP_ID ), expect_failure=True, ) - # Verify that no errors are thrown when device shown - # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices + + self._delete_device(device_id) + + 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) + + # wait about a few seconds for simulator to kick in so that provisioning completes + time.sleep(10) + self.cmd( - 'iot central app device-twin show --app-id "{}" --device-id "{}"'.format( - APP_ID, DEVICE_ID - ) + "iotcentral device-twin show --app-id {} --device-id {}".format( + APP_ID, device_id + ), + checks=[self.check("deviceId", device_id)], + ) + + self.cmd( + "iot central app device-twin show --app-id {} --device-id {}".format( + APP_ID, device_id + ), + checks=[self.check("deviceId", device_id)], ) + self._delete_device(device_id) + self._delete_device_template(template_id) + def test_central_monitor_events(self): # Test with invalid app-id self.cmd( @@ -91,20 +105,7 @@ def test_central_validate_messages(self): self.cmd("iot central app validate-messages --app-id {} --to 1".format(APP_ID)) def test_central_device_methods_CRLD(self): - device_id = self.create_random_name(prefix="aztest", length=24) - device_name = self.create_random_name(prefix="aztest", length=24) - # currently: create, show, list, delete - self.cmd( - "iot central app device create --app-id {} -d {} --device-name {}".format( - APP_ID, device_id, device_name - ), - checks=[ - self.check("approved", True), - self.check("displayName", device_name), - self.check("id", device_id), - self.check("simulated", False), - ], - ) + (device_id, device_name) = self._create_device() self.cmd( "iot central app device show --app-id {} -d {}".format(APP_ID, device_id), @@ -118,30 +119,13 @@ def test_central_device_methods_CRLD(self): list_output = self.cmd("iot central app device list --app-id {}".format(APP_ID)) - self.cmd( - "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), - checks=[self.check("result", "success")], - ) + self._delete_device(device_id) assert device_id in list_output.get_output_in_json() def test_central_device_template_methods_CRLD(self): # currently: create, show, list, delete - template = utility.process_json_arg( - DEVICE_TEMPLATE_PATH, argument_name="DEVICE_TEMPLATE_PATH" - ) - template_name = template["displayName"] - template_id = template_name + "id" - - self.cmd( - "iot central app device-template create --app-id {} --device-template-id {} -k {}".format( - APP_ID, template_id, DEVICE_TEMPLATE_PATH - ), - checks=[ - self.check("displayName", template_name), - self.check("id", template_id), - ], - ) + (template_id, template_name) = self._create_device_template() self.cmd( "iot central app device-template show --app-id {} --device-template-id {}".format( @@ -160,12 +144,7 @@ def test_central_device_template_methods_CRLD(self): "iot central app device-template map --app-id {}".format(APP_ID) ) - self.cmd( - "iot central app device-template delete --app-id {} --device-template-id {}".format( - APP_ID, template_id - ), - checks=[self.check("result", "success")], - ) + self._delete_device_template(template_id) assert template_id in list_output.get_output_in_json() @@ -173,20 +152,7 @@ def test_central_device_template_methods_CRLD(self): assert map_json[template_name] == template_id def test_central_device_registration_info(self): - device_id = self.create_random_name(prefix="aztest", length=24) - device_name = self.create_random_name(prefix="aztest", length=24) - # currently: create, show, list, delete - self.cmd( - "iot central app device create --app-id {} -d {} --device-name {}".format( - APP_ID, device_id, device_name - ), - checks=[ - self.check("approved", True), - self.check("displayName", device_name), - self.check("id", device_id), - self.check("simulated", False), - ], - ) + (device_id, _) = self._create_device() result = self.cmd( "iot central app device registration-info --app-id {} -d {}".format( @@ -194,10 +160,7 @@ def test_central_device_registration_info(self): ) ) - self.cmd( - "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), - checks=[self.check("result", "success")], - ) + self._delete_device(device_id) json_result = result.get_output_in_json() assert json_result["@device_id"] == device_id @@ -208,21 +171,9 @@ def test_central_device_registration_info(self): assert json_result["dps_state"] is not None def test_central_device_registration_info_filter_unassociated(self): - device_id = self.create_random_name(prefix="aztest", length=24) - device_name = self.create_random_name(prefix="aztest", length=24) device_status_expected = "unassociated" - self.cmd( - "iot central app device create --app-id {} -d {} --device-name {}".format( - APP_ID, device_id, device_name - ), - checks=[ - self.check("approved", True), - self.check("displayName", device_name), - self.check("id", device_id), - self.check("simulated", False), - ], - ) + (device_id, _) = self._create_device() result = self.cmd( "iot central app device registration-info --app-id {} --ds {}".format( @@ -230,10 +181,8 @@ def test_central_device_registration_info_filter_unassociated(self): ) ) - self.cmd( - "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), - checks=[self.check("result", "success")], - ) + self._delete_device(device_id) + json_result = [] device_info_results = [] json_result = result.get_output_in_json() @@ -244,16 +193,73 @@ def test_central_device_registration_info_filter_unassociated(self): assert device.get("deviceStatus") == device_status_expected def test_central_device_registration_info_filter_registered(self): + device_status_expected = "registered" + (template_id, _) = self._create_device_template() + (device_id, _) = self._create_device(instance_of=template_id) + + result = self.cmd( + "iot central app device registration-info --app-id {} --ds {}".format( + APP_ID, device_status_expected + ) + ) + + self._delete_device(device_id) + self._delete_device_template(template_id) + + json_result = [] + device_info_results = [] + json_result = result.get_output_in_json() + for device in json_result: + device_info_results.append(device.get("device_info")) + + for device in device_info_results: + assert device.get("deviceStatus") == device_status_expected + + def _create_device(self, **kwargs) -> (str, str): + """ + kwargs: + instance_of: template_id (str) + simulated: if the device is to be simulated (bool) + """ + device_id = self.create_random_name(prefix="aztest", length=24) + device_name = self.create_random_name(prefix="aztest", length=24) + + command = "iot central app device create --app-id {} -d {} --device-name {}".format( + APP_ID, device_id, device_name + ) + checks = [ + self.check("approved", 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)) + + simulated = bool(kwargs.get("simulated")) + if simulated: + command = command + " --simulated" + + checks.append(self.check("simulated", simulated)) + + self.cmd(command, checks=checks) + return (device_id, device_name) + + def _delete_device(self, device_id) -> None: + self.cmd( + "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), + checks=[self.check("result", "success")], + ) + + def _create_device_template(self): template = utility.process_json_arg( DEVICE_TEMPLATE_PATH, argument_name="DEVICE_TEMPLATE_PATH" ) template_name = template["displayName"] template_id = template_name + "id" - device_id = self.create_random_name(prefix="aztest", length=24) - device_name = self.create_random_name(prefix="aztest", length=24) - device_status_expected = "registered" - self.cmd( "iot central app device-template create --app-id {} --device-template-id {} -k {}".format( APP_ID, template_id, DEVICE_TEMPLATE_PATH @@ -264,41 +270,12 @@ def test_central_device_registration_info_filter_registered(self): ], ) - self.cmd( - "iot central app device create --app-id {} -d {} --device-name {} --instance-of {}".format( - APP_ID, device_id, device_name, template_id - ), - checks=[ - self.check("approved", True), - self.check("displayName", device_name), - self.check("id", device_id), - self.check("simulated", False), - ], - ) - - result = self.cmd( - "iot central app device registration-info --app-id {} --ds {}".format( - APP_ID, device_status_expected - ) - ) - - self.cmd( - "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), - checks=[self.check("result", "success")], - ) + return (template_id, template_name) + def _delete_device_template(self, template_id): self.cmd( "iot central app device-template delete --app-id {} --device-template-id {}".format( APP_ID, template_id ), checks=[self.check("result", "success")], ) - - json_result = [] - device_info_results = [] - json_result = result.get_output_in_json() - for device in json_result: - device_info_results.append(device.get("device_info")) - - for device in device_info_results: - assert device.get("deviceStatus") == device_status_expected From 93aae530989ef720b00a0bf7f914a1905f20927a Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Mon, 4 May 2020 12:39:11 -0700 Subject: [PATCH 028/179] Relax tightness of coupling in monitor code (#174) * First move * Clean up V1 * Reorganized code a bit * Fix flake8 issue, tried to test lack of uamqp --- .../events3 => monitor}/__init__.py | 0 azext_iot/monitor/builders/__init__.py | 5 + azext_iot/monitor/builders/_common.py | 69 +++ .../builders/central_target_builder.py | 26 + .../monitor/builders/hub_target_builder.py | 98 ++++ azext_iot/monitor/event.py | 143 ++++++ azext_iot/monitor/handlers/__init__.py | 9 + azext_iot/monitor/handlers/_internal.py | 66 +++ .../events3 => monitor/handlers}/_parser.py | 0 azext_iot/monitor/handlers/base_handler.py | 16 + azext_iot/monitor/handlers/handler.py | 54 +++ azext_iot/monitor/models/__init__.py | 5 + azext_iot/monitor/models/target.py | 23 + azext_iot/monitor/telemetry.py | 190 ++++++++ azext_iot/monitor/utility.py | 16 + azext_iot/operations/central.py | 28 +- azext_iot/operations/events3/_builders.py | 170 ------- azext_iot/operations/events3/_events.py | 459 ------------------ azext_iot/operations/hub.py | 42 +- azext_iot/tests/conftest.py | 50 +- azext_iot/tests/test_iot_utility_unit.py | 2 +- azext_iot/tests/test_uamqp_import.py | 29 ++ 22 files changed, 824 insertions(+), 676 deletions(-) rename azext_iot/{operations/events3 => monitor}/__init__.py (100%) create mode 100644 azext_iot/monitor/builders/__init__.py create mode 100644 azext_iot/monitor/builders/_common.py create mode 100644 azext_iot/monitor/builders/central_target_builder.py create mode 100644 azext_iot/monitor/builders/hub_target_builder.py create mode 100644 azext_iot/monitor/event.py create mode 100644 azext_iot/monitor/handlers/__init__.py create mode 100644 azext_iot/monitor/handlers/_internal.py rename azext_iot/{operations/events3 => monitor/handlers}/_parser.py (100%) create mode 100644 azext_iot/monitor/handlers/base_handler.py create mode 100644 azext_iot/monitor/handlers/handler.py create mode 100644 azext_iot/monitor/models/__init__.py create mode 100644 azext_iot/monitor/models/target.py create mode 100644 azext_iot/monitor/telemetry.py create mode 100644 azext_iot/monitor/utility.py delete mode 100644 azext_iot/operations/events3/_builders.py delete mode 100644 azext_iot/operations/events3/_events.py create mode 100644 azext_iot/tests/test_uamqp_import.py diff --git a/azext_iot/operations/events3/__init__.py b/azext_iot/monitor/__init__.py similarity index 100% rename from azext_iot/operations/events3/__init__.py rename to azext_iot/monitor/__init__.py diff --git a/azext_iot/monitor/builders/__init__.py b/azext_iot/monitor/builders/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/monitor/builders/__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/monitor/builders/_common.py b/azext_iot/monitor/builders/_common.py new file mode 100644 index 000000000..7ac3e0ddc --- /dev/null +++ b/azext_iot/monitor/builders/_common.py @@ -0,0 +1,69 @@ +# 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 uamqp +import urllib + +from azext_iot.monitor.models.target import Target + +DEBUG = False + + +async def convert_token_to_target(tokens) -> Target: + token_expiry = tokens["expiry"] + event_hub_token = tokens["eventhubSasToken"] + + sas_token = event_hub_token["sasToken"] + path = event_hub_token["entityPath"] + raw_url = event_hub_token["hostname"] + + url = urllib.parse.urlparse(raw_url) + hostname = url.hostname + + meta_data = await _query_meta_data_internal(hostname, path, sas_token, token_expiry) + + partition_count = meta_data[b"partition_count"] + partitions = [str(i) for i in range(int(partition_count))] + + auth = _build_auth_container_from_token(hostname, path, sas_token, token_expiry) + + return Target(hostname=hostname, path=path, partitions=partitions, auth=auth) + + +async def query_meta_data(address, path, auth): + source = uamqp.address.Source(address) + receive_client = uamqp.ReceiveClientAsync( + source, auth=auth, timeout=30000, debug=DEBUG + ) + try: + await receive_client.open_async() + message = uamqp.Message(application_properties={"name": path}) + + response = await receive_client.mgmt_request_async( + message, + b"READ", + op_type=b"com.microsoft:eventhub", + status_code_field=b"status-code", + description_fields=b"status-description", + timeout=30000, + ) + test = response.get_data() + return test + finally: + await receive_client.close_async() + + +async def _query_meta_data_internal(hostname, path, sas_token, token_expiry): + address = "amqps://{}/{}/$management".format(hostname, path) + auth = _build_auth_container_from_token(hostname, path, sas_token, token_expiry) + return await query_meta_data(address=address, path=path, auth=auth) + + +def _build_auth_container_from_token(hostname, path, token, expiry): + sas_uri = "sb://{}/{}".format(hostname, path) + return uamqp.authentication.SASTokenAsync( + audience=sas_uri, uri=sas_uri, expires_at=expiry, token=token + ) diff --git a/azext_iot/monitor/builders/central_target_builder.py b/azext_iot/monitor/builders/central_target_builder.py new file mode 100644 index 000000000..8d24c8a48 --- /dev/null +++ b/azext_iot/monitor/builders/central_target_builder.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. +# -------------------------------------------------------------------------------------------- +import asyncio + +from typing import List + +from azext_iot.common._azure import get_iot_central_tokens +from azext_iot.monitor.models.target import Target +from azext_iot.monitor.builders._common import convert_token_to_target + + +def build_central_event_hub_targets(cmd, app_id, central_api_uri) -> List[Target]: + event_loop = asyncio.get_event_loop() + return event_loop.run_until_complete( + _build_central_event_hub_targets_async(cmd, app_id, central_api_uri) + ) + + +async def _build_central_event_hub_targets_async(cmd, app_id, central_api_uri): + all_tokens = get_iot_central_tokens(cmd, app_id, central_api_uri) + targets = [await convert_token_to_target(token) for token in all_tokens.values()] + + return targets diff --git a/azext_iot/monitor/builders/hub_target_builder.py b/azext_iot/monitor/builders/hub_target_builder.py new file mode 100644 index 000000000..bc61bc3fd --- /dev/null +++ b/azext_iot/monitor/builders/hub_target_builder.py @@ -0,0 +1,98 @@ +# 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 asyncio +import uamqp + +from azext_iot.common.sas_token_auth import SasTokenAuthentication +from azext_iot.common.utility import parse_entity, unicode_binary_map, url_encode_str +from azext_iot.monitor.builders._common import query_meta_data +from azext_iot.monitor.models.target import Target + +# To provide amqp frame trace +DEBUG = False + + +class AmqpBuilder: + @classmethod + def build_iothub_amqp_endpoint_from_target(cls, target, duration=360): + hub_name = target["entity"].split(".")[0] + user = "{}@sas.root.{}".format(target["policy"], hub_name) + sas_token = SasTokenAuthentication( + target["entity"], target["policy"], target["primarykey"], duration + ).generate_sas_token() + return url_encode_str(user) + ":{}@{}".format( + url_encode_str(sas_token), target["entity"] + ) + + +class EventTargetBuilder: + def __init__(self): + self.eventLoop = asyncio.new_event_loop() + asyncio.set_event_loop(self.eventLoop) + + def build_iot_hub_target(self, target): + return self.eventLoop.run_until_complete( + self._build_iot_hub_target_async(target) + ) + + def _build_auth_container(self, target): + sas_uri = "sb://{}/{}".format( + target["events"]["endpoint"], target["events"]["path"] + ) + return uamqp.authentication.SASTokenAsync.from_shared_access_key( + sas_uri, target["policy"], target["primarykey"] + ) + + async def _evaluate_redirect(self, endpoint): + source = uamqp.address.Source( + "amqps://{}/messages/events/$management".format(endpoint) + ) + receive_client = uamqp.ReceiveClientAsync( + source, timeout=30000, prefetch=1, debug=DEBUG + ) + + try: + await receive_client.open_async() + await receive_client.receive_message_batch_async(max_batch_size=1) + except uamqp.errors.LinkRedirect as redirect: + redirect = unicode_binary_map(parse_entity(redirect)) + result = {} + result["events"] = {} + result["events"]["endpoint"] = redirect["hostname"] + result["events"]["path"] = ( + redirect["address"].replace("amqps://", "").split("/")[1] + ) + result["events"]["address"] = redirect["address"] + return redirect, result + finally: + await receive_client.close_async() + + async def _build_iot_hub_target_async(self, target): + if "events" not in target: + endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target(target) + _, update = await self._evaluate_redirect(endpoint) + target["events"] = update["events"] + endpoint = target["events"]["endpoint"] + path = target["events"]["path"] + auth = self._build_auth_container(target) + meta_data = await query_meta_data( + address=target["events"]["address"], + path=target["events"]["path"], + auth=auth, + ) + partition_count = meta_data[b"partition_count"] + partition_ids = [] + for i in range(int(partition_count)): + partition_ids.append(str(i)) + target["events"]["partition_ids"] = partition_ids + else: + endpoint = target["events"]["endpoint"] + path = target["events"]["path"] + partitions = target["events"]["partition_ids"] + auth = self._build_auth_container(target) + + return Target(hostname=endpoint, path=path, partitions=partitions, auth=auth) diff --git a/azext_iot/monitor/event.py b/azext_iot/monitor/event.py new file mode 100644 index 000000000..faa7afa85 --- /dev/null +++ b/azext_iot/monitor/event.py @@ -0,0 +1,143 @@ +# 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 uamqp +import yaml + +from uuid import uuid4 +from knack.log import get_logger +from azext_iot.constants import USER_AGENT +from azext_iot.common.utility import process_json_arg +from azext_iot.monitor.builders.hub_target_builder import AmqpBuilder + +# To provide amqp frame trace +DEBUG = False +logger = get_logger(__name__) + + +def send_c2d_message( + target, + device_id, + data, + message_id=None, + correlation_id=None, + ack=None, + content_type=None, + user_id=None, + content_encoding="utf-8", + expiry_time_utc=None, + properties=None, +): + + app_props = {} + if properties: + app_props.update(properties) + + app_props["iothub-ack"] = ack if ack else "none" + + msg_props = uamqp.message.MessageProperties() + msg_props.to = "/devices/{}/messages/devicebound".format(device_id) + + target_msg_id = message_id if message_id else str(uuid4()) + msg_props.message_id = target_msg_id + + if correlation_id: + msg_props.correlation_id = correlation_id + + if user_id: + msg_props.user_id = user_id + + if content_type: + msg_props.content_type = content_type + + # Ensures valid json when content_type is application/json + content_type = content_type.lower() + if content_type == "application/json": + data = json.dumps(process_json_arg(data, "data")) + + if content_encoding: + msg_props.content_encoding = content_encoding + + if expiry_time_utc: + msg_props.absolute_expiry_time = int(expiry_time_utc) + + msg_body = str.encode(data) + + message = uamqp.Message( + body=msg_body, properties=msg_props, application_properties=app_props + ) + + operation = "/messages/devicebound" + endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target(target) + endpoint_with_op = endpoint + operation + client = uamqp.SendClient( + target="amqps://" + endpoint_with_op, + client_name=_get_container_id(), + debug=DEBUG, + ) + client.queue_message(message) + result = client.send_all_messages() + errors = [m for m in result if m == uamqp.constants.MessageState.SendFailed] + return target_msg_id, errors + + +def monitor_feedback(target, device_id, wait_on_id=None, token_duration=3600): + def handle_msg(msg): + payload = next(msg.get_data()) + if isinstance(payload, bytes): + payload = str(payload, "utf8") + # assume json [] based on spec + payload = json.loads(payload) + for p in payload: + if ( + device_id + and p.get("deviceId") + and p["deviceId"].lower() != device_id.lower() + ): + return None + print(yaml.dump({"feedback": p}, default_flow_style=False), flush=True) + if wait_on_id: + msg_id = p["originalMessageId"] + if msg_id == wait_on_id: + return msg_id + return None + + operation = "/messages/servicebound/feedback" + endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target( + target, duration=token_duration + ) + 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 "" + ) + ) + + try: + client = uamqp.ReceiveClient( + "amqps://" + endpoint, client_name=_get_container_id(), debug=DEBUG + ) + message_generator = client.receive_messages_iter() + for msg in message_generator: + match = handle_msg(msg) + if match: + logger.info("Requested message Id has been matched...") + msg.accept() + return match + except uamqp.errors.AMQPConnectionError: + logger.debug("AMQPS connection has expired...") + finally: + client.close() + + +def _get_container_id(): + return "{}/{}".format(USER_AGENT, str(uuid4())) diff --git a/azext_iot/monitor/handlers/__init__.py b/azext_iot/monitor/handlers/__init__.py new file mode 100644 index 000000000..f8648d3e5 --- /dev/null +++ b/azext_iot/monitor/handlers/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.monitor.handlers.handler import CommonHandler + +__all__ = ["CommonHandler"] diff --git a/azext_iot/monitor/handlers/_internal.py b/azext_iot/monitor/handlers/_internal.py new file mode 100644 index 000000000..714c12c8f --- /dev/null +++ b/azext_iot/monitor/handlers/_internal.py @@ -0,0 +1,66 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +import re +import yaml + +from azext_iot.monitor.handlers._parser import Event3Parser + + +def parse_message( + msg, + device_id, + devices, + pnp_context, + interface_name, + content_type, + properties, + output, + validate_messages, + simulate_errors, + central_device_provider, +): + parser = Event3Parser() + origin_device_id = parser.parse_device_id(msg) + + if not _should_process_device(origin_device_id, device_id, devices): + return + + parsed_msg = parser.parse_message( + msg, + pnp_context, + interface_name, + properties, + content_type, + simulate_errors, + central_device_provider, + ) + + if output.lower() == "json": + dump = json.dumps(parsed_msg, indent=4) + else: + dump = yaml.safe_dump(parsed_msg, default_flow_style=False) + + if validate_messages: + parser.write_logs() + + if not validate_messages: + print(dump, flush=True) + + +def _should_process_device(origin_device_id, device_id, devices): + if device_id and device_id != origin_device_id: + if "*" in device_id or "?" in device_id: + regex = re.escape(device_id).replace("\\*", ".*").replace("\\?", ".") + "$" + if not re.match(regex, origin_device_id): + return False + else: + return False + if devices and origin_device_id not in devices: + return False + + return True diff --git a/azext_iot/operations/events3/_parser.py b/azext_iot/monitor/handlers/_parser.py similarity index 100% rename from azext_iot/operations/events3/_parser.py rename to azext_iot/monitor/handlers/_parser.py diff --git a/azext_iot/monitor/handlers/base_handler.py b/azext_iot/monitor/handlers/base_handler.py new file mode 100644 index 000000000..409697fb2 --- /dev/null +++ b/azext_iot/monitor/handlers/base_handler.py @@ -0,0 +1,16 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from abc import ABC, abstractmethod + + +class AbstractBaseEventsHandler(ABC): + def __init__(self): + super().__init__() + + @abstractmethod + def parse_message(self, message): + pass diff --git a/azext_iot/monitor/handlers/handler.py b/azext_iot/monitor/handlers/handler.py new file mode 100644 index 000000000..57df4bf72 --- /dev/null +++ b/azext_iot/monitor/handlers/handler.py @@ -0,0 +1,54 @@ +# 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.monitor.handlers import _internal +from azext_iot.monitor.handlers.base_handler import AbstractBaseEventsHandler + +""" +Use this handler if you aren't sure which handler is right for you +""" + + +class CommonHandler(AbstractBaseEventsHandler): + def __init__( + self, + device_id=None, + devices=None, + pnp_context=None, + interface_name=None, + content_type=None, + properties=None, + output=None, + validate_messages=None, + simulate_errors=None, + central_device_provider=None, + ): + self.device_id = device_id + self.devices = devices + self.pnp_context = pnp_context + self.interface_name = interface_name + self.content_type = content_type + self.properties = properties + self.output = output + self.validate_messages = validate_messages + self.simulate_errors = simulate_errors + self.central_device_provider = central_device_provider + pass + + def parse_message(self, message): + _internal.parse_message( + message, + device_id=self.device_id, + devices=self.devices, + pnp_context=self.pnp_context, + interface_name=self.interface_name, + content_type=self.content_type, + properties=self.properties, + output=self.output, + validate_messages=self.validate_messages, + simulate_errors=self.simulate_errors, + central_device_provider=self.central_device_provider, + ) diff --git a/azext_iot/monitor/models/__init__.py b/azext_iot/monitor/models/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/monitor/models/__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/monitor/models/target.py b/azext_iot/monitor/models/target.py new file mode 100644 index 000000000..bb9746ac2 --- /dev/null +++ b/azext_iot/monitor/models/target.py @@ -0,0 +1,23 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +class Target: + def __init__( + self, + hostname: str, + path: str, + partitions: list, + auth, # : uamqp.authentication.SASTokenAsync, + ): + self.hostname = hostname + self.path = path + self.auth = auth + self.partitions = partitions + self.consumer_group = None + + def add_consumer_group(self, consumer_group: str): + self.consumer_group = consumer_group diff --git a/azext_iot/monitor/telemetry.py b/azext_iot/monitor/telemetry.py new file mode 100644 index 000000000..598da2958 --- /dev/null +++ b/azext_iot/monitor/telemetry.py @@ -0,0 +1,190 @@ +# 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 asyncio +import sys +import uamqp + +from uuid import uuid4 +from knack.log import get_logger +from typing import List +from azext_iot.constants import VERSION, USER_AGENT +from azext_iot.monitor.models.target import Target + +logger = get_logger(__name__) +DEBUG = False + + +def start_single_monitor( + target: Target, + enqueued_time_utc, + on_start_string: str, + on_message_received, + timeout=0, +): + """ + :param on_message_received: + A callback to process messages as they arrive from the service. + It takes a single argument, a ~uamqp.message.Message object. + """ + return start_multiple_monitors( + targets=[target], + enqueued_time_utc=enqueued_time_utc, + on_start_string=on_start_string, + on_message_received=on_message_received, + timeout=timeout, + ) + + +def start_multiple_monitors( + targets: List[Target], + on_start_string: str, + enqueued_time_utc, + on_message_received, + timeout=0, +): + """ + :param on_message_received: + A callback to process messages as they arrive from the service. + It takes a single argument, a ~uamqp.message.Message object. + """ + coroutines = [ + _initiate_event_monitor( + target=target, + enqueued_time_utc=enqueued_time_utc, + on_message_received=on_message_received, + timeout=timeout, + ) + for target in targets + ] + + loop = asyncio.get_event_loop() + if loop.is_closed(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + future = asyncio.gather(*coroutines, loop=loop, return_exceptions=True) + result = None + + try: + print(on_start_string, flush=True) + future.add_done_callback(lambda future: _stop_and_suppress_eloop(loop)) + result = loop.run_until_complete(future) + except KeyboardInterrupt: + print("Stopping event monitor...", flush=True) + for t in asyncio.Task.all_tasks(): + t.cancel() + loop.run_forever() + finally: + if result: + errors = result[0] + if errors and errors[0]: + logger.debug(errors) + raise RuntimeError(errors[0]) + + +async def _initiate_event_monitor( + target: Target, enqueued_time_utc, on_message_received, timeout=0 +): + if not target.partitions: + logger.debug("No Event Hub partitions found to listen on.") + return + + coroutines = [] + + async with uamqp.ConnectionAsync( + target.hostname, + sasl=target.auth, + debug=DEBUG, + container_id=_get_container_id(), + properties=_get_conn_props(), + ) as conn: + for p in target.partitions: + coroutines.append( + _monitor_events( + target=target, + connection=conn, + partition=p, + enqueued_time_utc=enqueued_time_utc, + on_message_received=on_message_received, + timeout=timeout, + ) + ) + return await asyncio.gather(*coroutines, return_exceptions=True) + + +async def _monitor_events( + target: Target, + connection, + partition, + enqueued_time_utc, + on_message_received, + timeout=0, +): + source = uamqp.address.Source( + "amqps://{}/{}/ConsumerGroups/{}/Partitions/{}".format( + target.hostname, target.path, target.consumer_group, partition + ) + ) + source.set_filter( + bytes( + "amqp.annotation.x-opt-enqueuedtimeutc > " + str(enqueued_time_utc), "utf8" + ) + ) + + exp_cancelled = False + receive_client = uamqp.ReceiveClientAsync( + source, + auth=target.auth, + timeout=timeout, + prefetch=0, + client_name=_get_container_id(), + debug=DEBUG, + ) + + try: + if connection: + await receive_client.open_async(connection=connection) + + async for msg in receive_client.receive_messages_iter_async(): + on_message_received(msg) + + except asyncio.CancelledError: + exp_cancelled = True + await receive_client.close_async() + except uamqp.errors.LinkDetach as ld: + if isinstance(ld.description, bytes): + ld.description = str(ld.description, "utf8") + raise RuntimeError(ld.description) + except KeyboardInterrupt: + logger.info("Keyboard interrupt, closing monitor on partition %s", partition) + exp_cancelled = True + await receive_client.close_async() + raise + finally: + if not exp_cancelled: + await receive_client.close_async() + logger.info("Closed monitor on partition %s", partition) + + +def _stop_and_suppress_eloop(loop): + try: + loop.stop() + except Exception: + pass + + +def _get_conn_props(): + return { + "product": USER_AGENT, + "version": VERSION, + "framework": "Python {}.{}.{}".format(*sys.version_info[0:3]), + "platform": sys.platform, + } + + +def _get_container_id(): + return "{}/{}".format(USER_AGENT, str(uuid4())) diff --git a/azext_iot/monitor/utility.py b/azext_iot/monitor/utility.py new file mode 100644 index 000000000..5155f53cd --- /dev/null +++ b/azext_iot/monitor/utility.py @@ -0,0 +1,16 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def generate_on_start_string(device_id, pnp_context): + device_filter_txt = None + if device_id: + device_filter_txt = " filtering on device: {},".format(device_id) + + return "Starting {}event monitor,{} use ctrl-c to stop...".format( + "Digital Twin " if pnp_context else "", + device_filter_txt if device_filter_txt else "", + ) diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py index 63f1ae177..4c234464a 100644 --- a/azext_iot/operations/central.py +++ b/azext_iot/operations/central.py @@ -120,24 +120,24 @@ def _events3_runner( cmd, timeout, properties, enqueued_time, repair, yes ) - from azext_iot.operations.events3 import _builders, _events + from azext_iot.monitor.builders import central_target_builder + from azext_iot.monitor.handlers import CommonHandler + from azext_iot.monitor.utility import generate_on_start_string + from azext_iot.monitor import telemetry - event_hub_targets = _builders.EventTargetBuilder().build_central_event_hub_target( + on_start_string = generate_on_start_string(device_id=device_id, pnp_context=None) + + targets = central_target_builder.build_central_event_hub_targets( cmd, app_id, central_api_uri ) - executorTargets = [] + [target.add_consumer_group(consumer_group) for target in targets] - for target in event_hub_targets: - executorTargets.append(_events.executorData(target, consumer_group)) + handler = CommonHandler(device_id=device_id, properties=properties, output=output) - _events.nExecutor( - executorTargets, - enqueued_time=enqueued_time, - properties=properties, + telemetry.start_multiple_monitors( + targets=targets, + enqueued_time_utc=enqueued_time, + on_start_string=on_start_string, + on_message_received=handler.parse_message, timeout=timeout, - device_id=device_id, - output=output, - validate_messages=validate_messages, - simulate_errors=simulate_errors, - central_device_provider=central_device_provider, ) diff --git a/azext_iot/operations/events3/_builders.py b/azext_iot/operations/events3/_builders.py deleted file mode 100644 index a67527a13..000000000 --- a/azext_iot/operations/events3/_builders.py +++ /dev/null @@ -1,170 +0,0 @@ -import asyncio -import uamqp - -from azext_iot.common.sas_token_auth import SasTokenAuthentication -from azext_iot.common.utility import parse_entity, unicode_binary_map, url_encode_str - -# To provide amqp frame trace -DEBUG = False - - -class AmqpBuilder: - @classmethod - def build_iothub_amqp_endpoint_from_target(cls, target, duration=360): - hub_name = target["entity"].split(".")[0] - user = "{}@sas.root.{}".format(target["policy"], hub_name) - sas_token = SasTokenAuthentication( - target["entity"], target["policy"], target["primarykey"], duration - ).generate_sas_token() - return url_encode_str(user) + ":{}@{}".format( - url_encode_str(sas_token), target["entity"] - ) - - -class EventTargetBuilder: - def __init__(self): - self.eventLoop = asyncio.new_event_loop() - asyncio.set_event_loop(self.eventLoop) - - def build_iot_hub_target(self, target): - return self.eventLoop.run_until_complete( - self._build_iot_hub_target_async(target) - ) - - def build_central_event_hub_target(self, cmd, app_id, central_api_uri): - return self.eventLoop.run_until_complete( - self._build_central_event_hub_target_async(cmd, app_id, central_api_uri) - ) - - def _build_auth_container(self, target): - sas_uri = "sb://{}/{}".format( - target["events"]["endpoint"], target["events"]["path"] - ) - return uamqp.authentication.SASTokenAsync.from_shared_access_key( - sas_uri, target["policy"], target["primarykey"] - ) - - def _build_auth_container_from_token(self, endpoint, path, token, tokenExpiry): - sas_uri = "sb://{}/{}".format(endpoint, path) - return uamqp.authentication.SASTokenAsync( - audience=sas_uri, uri=sas_uri, expires_at=tokenExpiry, token=token - ) - - async def _query_meta_data(self, endpoint, path, auth): - source = uamqp.address.Source(endpoint) - receive_client = uamqp.ReceiveClientAsync( - source, auth=auth, timeout=30000, debug=DEBUG - ) - try: - await receive_client.open_async() - message = uamqp.Message(application_properties={"name": path}) - - response = await receive_client.mgmt_request_async( - message, - b"READ", - op_type=b"com.microsoft:eventhub", - status_code_field=b"status-code", - description_fields=b"status-description", - timeout=30000, - ) - test = response.get_data() - return test - finally: - await receive_client.close_async() - - async def _evaluate_redirect(self, endpoint): - source = uamqp.address.Source( - "amqps://{}/messages/events/$management".format(endpoint) - ) - receive_client = uamqp.ReceiveClientAsync( - source, timeout=30000, prefetch=1, debug=DEBUG - ) - - try: - await receive_client.open_async() - await receive_client.receive_message_batch_async(max_batch_size=1) - except uamqp.errors.LinkRedirect as redirect: - redirect = unicode_binary_map(parse_entity(redirect)) - result = {} - result["events"] = {} - result["events"]["endpoint"] = redirect["hostname"] - result["events"]["path"] = ( - redirect["address"].replace("amqps://", "").split("/")[1] - ) - result["events"]["address"] = redirect["address"] - return redirect, result - finally: - await receive_client.close_async() - - async def create_single_iotc_eventhub_target_async(self, tokens): - event_hub_token = tokens["eventhubSasToken"] - hostname_without_prefix = event_hub_token["hostname"].split("/")[2] - endpoint = hostname_without_prefix - path = event_hub_token["entityPath"] - token_expiry = tokens["expiry"] - auth = self._build_auth_container_from_token( - endpoint, path, event_hub_token["sasToken"], token_expiry - ) - address = "amqps://{}/{}/$management".format( - hostname_without_prefix, event_hub_token["entityPath"] - ) - meta_data = await self._query_meta_data(address, path, auth) - partition_count = meta_data[b"partition_count"] - partition_ids = [] - for i in range(int(partition_count)): - partition_ids.append(str(i)) - partitions = partition_ids - auth = self._build_auth_container_from_token( - endpoint, path, event_hub_token["sasToken"], token_expiry - ) - - event_hub_target = { - "endpoint": endpoint, - "path": path, - "auth": auth, - "partitions": partitions, - } - - return event_hub_target - - async def _build_central_event_hub_target_async(self, cmd, app_id, central_api_uri): - from azext_iot.common._azure import get_iot_central_tokens - - all_tokens = get_iot_central_tokens(cmd, app_id, central_api_uri) - targets = [ - await self.create_single_iotc_eventhub_target_async(tokens) - for tokens in all_tokens.values() - ] - - return targets - - async def _build_iot_hub_target_async(self, target): - if "events" not in target: - endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target(target) - _, update = await self._evaluate_redirect(endpoint) - target["events"] = update["events"] - endpoint = target["events"]["endpoint"] - path = target["events"]["path"] - auth = self._build_auth_container(target) - meta_data = await self._query_meta_data( - target["events"]["address"], target["events"]["path"], auth - ) - partition_count = meta_data[b"partition_count"] - partition_ids = [] - for i in range(int(partition_count)): - partition_ids.append(str(i)) - target["events"]["partition_ids"] = partition_ids - else: - endpoint = target["events"]["endpoint"] - path = target["events"]["path"] - partitions = target["events"]["partition_ids"] - auth = self._build_auth_container(target) - - eventHubTarget = { - "endpoint": endpoint, - "path": path, - "auth": auth, - "partitions": partitions, - } - - return eventHubTarget diff --git a/azext_iot/operations/events3/_events.py b/azext_iot/operations/events3/_events.py deleted file mode 100644 index 2bf63e701..000000000 --- a/azext_iot/operations/events3/_events.py +++ /dev/null @@ -1,459 +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. -# -------------------------------------------------------------------------------------------- - -import asyncio -import json -import re -import sys -import six -import yaml -import uamqp - -from uuid import uuid4 -from knack.log import get_logger -from azext_iot.constants import VERSION, USER_AGENT -from azext_iot.common.utility import process_json_arg -from azext_iot.operations.events3._builders import AmqpBuilder -from azext_iot.operations.events3._parser import Event3Parser - -# To provide amqp frame trace -DEBUG = False -logger = get_logger(__name__) - - -class executorData: - def __init__(self, target, consumer_group): - self.target = target - self.consumer_group = consumer_group - - -def executor( - target, - consumer_group, - enqueued_time, - properties=None, - timeout=0, - device_id=None, - output=None, - content_type=None, - devices=None, - interface_name=None, - pnp_context=None, - validate_messages=False, - simulate_errors=False, - central_device_provider=None, -): - executor = executorData(target, consumer_group) - - return nExecutor( - [executor], - enqueued_time, - properties, - timeout, - device_id, - output, - content_type, - devices, - interface_name, - pnp_context, - validate_messages, - simulate_errors, - central_device_provider, - ) - - -def nExecutor( - executorTargets, - enqueued_time, - properties=None, - timeout=0, - device_id=None, - output=None, - content_type=None, - devices=None, - interface_name=None, - pnp_context=None, - validate_messages=False, - simulate_errors=False, - central_device_provider=None, -): - coroutines = [] - - for executor in executorTargets: - coroutines.append( - initiate_event_monitor( - executor.target, - executor.consumer_group, - enqueued_time, - device_id, - properties, - timeout, - output, - content_type, - devices, - interface_name, - pnp_context, - validate_messages, - simulate_errors, - central_device_provider, - ) - ) - - loop = asyncio.get_event_loop() - if loop.is_closed(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - future = asyncio.gather(*coroutines, loop=loop, return_exceptions=True) - result = None - - try: - device_filter_txt = None - if device_id: - device_filter_txt = " filtering on device: {},".format(device_id) - - def stop_and_suppress_eloop(): - try: - loop.stop() - except Exception: - pass - - six.print_( - "Starting {}event monitor,{} use ctrl-c to stop...".format( - "Digital Twin " if pnp_context else "", - device_filter_txt if device_filter_txt else "", - ) - ) - future.add_done_callback(lambda future: stop_and_suppress_eloop()) - result = loop.run_until_complete(future) - except KeyboardInterrupt: - six.print_("Stopping event monitor...") - for t in asyncio.Task.all_tasks(): - t.cancel() - loop.run_forever() - finally: - if result: - errors = result[0] - if errors and errors[0]: - logger.debug(errors) - raise RuntimeError(errors[0]) - - -async def initiate_event_monitor( - target, - consumer_group, - enqueued_time, - device_id=None, - properties=None, - timeout=0, - output=None, - content_type=None, - devices=None, - interface_name=None, - pnp_context=None, - validate_messages=False, - simulate_errors=False, - central_device_provider=None, -): - def _get_conn_props(): - properties = {} - properties["product"] = USER_AGENT - properties["version"] = VERSION - properties["framework"] = "Python {}.{}.{}".format(*sys.version_info[0:3]) - properties["platform"] = sys.platform - return properties - - if not target["partitions"]: - logger.debug("No Event Hub partitions found to listen on.") - return - - coroutines = [] - - async with uamqp.ConnectionAsync( - target["endpoint"], - sasl=target["auth"], - debug=DEBUG, - container_id=_get_container_id(), - properties=_get_conn_props(), - ) as conn: - for p in target["partitions"]: - coroutines.append( - monitor_events( - endpoint=target["endpoint"], - connection=conn, - path=target["path"], - auth=target["auth"], - partition=p, - consumer_group=consumer_group, - enqueuedtimeutc=enqueued_time, - properties=properties, - device_id=device_id, - timeout=timeout, - output=output, - content_type=content_type, - devices=devices, - interface_name=interface_name, - pnp_context=pnp_context, - validate_messages=validate_messages, - simulate_errors=simulate_errors, - central_device_provider=central_device_provider, - ) - ) - return await asyncio.gather(*coroutines, return_exceptions=True) - - -async def monitor_events( - endpoint, - connection, - path, - auth, - partition, - consumer_group, - enqueuedtimeutc, - properties, - device_id=None, - timeout=0, - output=None, - content_type=None, - devices=None, - interface_name=None, - pnp_context=None, - validate_messages=False, - simulate_errors=False, - central_device_provider=None, -): - source = uamqp.address.Source( - "amqps://{}/{}/ConsumerGroups/{}/Partitions/{}".format( - endpoint, path, consumer_group, partition - ) - ) - source.set_filter( - bytes("amqp.annotation.x-opt-enqueuedtimeutc > " + str(enqueuedtimeutc), "utf8") - ) - - exp_cancelled = False - receive_client = uamqp.ReceiveClientAsync( - source, - auth=auth, - timeout=timeout, - prefetch=0, - client_name=_get_container_id(), - debug=DEBUG, - ) - - try: - if connection: - await receive_client.open_async(connection=connection) - - async for msg in receive_client.receive_messages_iter_async(): - _output_msg_kpi( - msg, - device_id, - devices, - pnp_context, - interface_name, - content_type, - properties, - output, - validate_messages, - simulate_errors, - central_device_provider, - ) - - except asyncio.CancelledError: - exp_cancelled = True - await receive_client.close_async() - except uamqp.errors.LinkDetach as ld: - if isinstance(ld.description, bytes): - ld.description = str(ld.description, "utf8") - raise RuntimeError(ld.description) - except KeyboardInterrupt: - logger.info("Keyboard interrupt, closing monitor on partition %s", partition) - exp_cancelled = True - await receive_client.close_async() - raise - finally: - if not exp_cancelled: - await receive_client.close_async() - logger.info("Closed monitor on partition %s", partition) - - -def send_c2d_message( - target, - device_id, - data, - message_id=None, - correlation_id=None, - ack=None, - content_type=None, - user_id=None, - content_encoding="utf-8", - expiry_time_utc=None, - properties=None, -): - app_props = {} - if properties: - app_props.update(properties) - - app_props["iothub-ack"] = ack if ack else "none" - - msg_props = uamqp.message.MessageProperties() - msg_props.to = "/devices/{}/messages/devicebound".format(device_id) - - target_msg_id = message_id if message_id else str(uuid4()) - msg_props.message_id = target_msg_id - - if correlation_id: - msg_props.correlation_id = correlation_id - - if user_id: - msg_props.user_id = user_id - - if content_type: - msg_props.content_type = content_type - - # Ensures valid json when content_type is application/json - content_type = content_type.lower() - if content_type == "application/json": - data = json.dumps(process_json_arg(data, "data")) - - if content_encoding: - msg_props.content_encoding = content_encoding - - if expiry_time_utc: - msg_props.absolute_expiry_time = int(expiry_time_utc) - - msg_body = str.encode(data) - - message = uamqp.Message( - body=msg_body, properties=msg_props, application_properties=app_props - ) - - operation = "/messages/devicebound" - endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target(target) - endpoint_with_op = endpoint + operation - client = uamqp.SendClient( - target="amqps://" + endpoint_with_op, - client_name=_get_container_id(), - debug=DEBUG, - ) - client.queue_message(message) - result = client.send_all_messages() - errors = [m for m in result if m == uamqp.constants.MessageState.SendFailed] - return target_msg_id, errors - - -def monitor_feedback(target, device_id, wait_on_id=None, token_duration=3600): - def handle_msg(msg): - payload = next(msg.get_data()) - if isinstance(payload, bytes): - payload = str(payload, "utf8") - # assume json [] based on spec - payload = json.loads(payload) - for p in payload: - if ( - device_id - and p.get("deviceId") - and p["deviceId"].lower() != device_id.lower() - ): - return None - six.print_(yaml.dump({"feedback": p}, default_flow_style=False), flush=True) - if wait_on_id: - msg_id = p["originalMessageId"] - if msg_id == wait_on_id: - return msg_id - return None - - operation = "/messages/servicebound/feedback" - endpoint = AmqpBuilder.build_iothub_amqp_endpoint_from_target( - target, duration=token_duration - ) - endpoint = endpoint + operation - - device_filter_txt = None - if device_id: - device_filter_txt = " filtering on device: {},".format(device_id) - - six.print_( - "Starting C2D feedback monitor,{} use ctrl-c to stop...".format( - device_filter_txt if device_filter_txt else "" - ) - ) - - try: - client = uamqp.ReceiveClient( - "amqps://" + endpoint, client_name=_get_container_id(), debug=DEBUG - ) - message_generator = client.receive_messages_iter() - for msg in message_generator: - match = handle_msg(msg) - if match: - logger.info("Requested message Id has been matched...") - msg.accept() - return match - except uamqp.errors.AMQPConnectionError: - logger.debug("AMQPS connection has expired...") - finally: - client.close() - - -def _get_container_id(): - return "{}/{}".format(USER_AGENT, str(uuid4())) - - -def _output_msg_kpi( - msg, - device_id, - devices, - pnp_context, - interface_name, - content_type, - properties, - output, - validate_messages, - simulate_errors, - central_device_provider, -): - parser = Event3Parser() - origin_device_id = parser.parse_device_id(msg) - - if not _should_process_device(origin_device_id, device_id, devices): - return - - parsed_msg = parser.parse_message( - msg, - pnp_context, - interface_name, - properties, - content_type, - simulate_errors, - central_device_provider, - ) - - if output.lower() == "json": - dump = json.dumps(parsed_msg, indent=4) - else: - dump = yaml.safe_dump(parsed_msg, default_flow_style=False) - - if validate_messages: - parser.write_logs() - - if not validate_messages: - six.print_(dump, flush=True) - - -def _should_process_device(origin_device_id, device_id, devices): - if device_id and device_id != origin_device_id: - if "*" in device_id or "?" in device_id: - regex = re.escape(device_id).replace("\\*", ".*").replace("\\?", ".") + "$" - if not re.match(regex, origin_device_id): - return False - else: - return False - if devices and origin_device_id not in devices: - return False - - return True diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index b031f8a1a..ba5272e1e 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -1892,9 +1892,9 @@ def iot_c2d_message_send( if user_msg_expiry < now_in_milli: raise CLIError("Message expiry time utc is in the past!") - from azext_iot.operations.events3 import _events + from azext_iot.monitor import event - msg_id, errors = _events.send_c2d_message( + msg_id, errors = event.send_c2d_message( target=target, device_id=device_id, data=data, @@ -2232,22 +2232,34 @@ def _iot_hub_monitor_events( cmd, hub_name, resource_group_name, include_events=True, login=login ) - from azext_iot.operations.events3 import _builders, _events + from azext_iot.monitor.builders import hub_target_builder + from azext_iot.monitor.handlers import CommonHandler + from azext_iot.monitor.telemetry import start_single_monitor + from azext_iot.monitor.utility import generate_on_start_string - eventHubTarget = _builders.EventTargetBuilder().build_iot_hub_target(target) + target = hub_target_builder.EventTargetBuilder().build_iot_hub_target(target) + target.add_consumer_group(consumer_group) - _events.executor( - eventHubTarget, - consumer_group=consumer_group, - enqueued_time=enqueued_time, - properties=properties, - timeout=timeout, + on_start_string = generate_on_start_string( + device_id=device_id, pnp_context=pnp_context + ) + + handler = CommonHandler( device_id=device_id, - output=output, - content_type=content_type, devices=device_ids, - interface_name=interface_name, pnp_context=pnp_context, + interface_name=interface_name, + content_type=content_type, + properties=properties, + output=output, + ) + + start_single_monitor( + target=target, + enqueued_time_utc=enqueued_time, + on_start_string=on_start_string, + on_message_received=handler.parse_message, + timeout=timeout, ) @@ -2278,9 +2290,9 @@ def iot_hub_distributed_tracing_update( def _iot_hub_monitor_feedback(target, device_id, wait_on_id): - from azext_iot.operations.events3 import _events + from azext_iot.monitor import event - _events.monitor_feedback( + event.monitor_feedback( target=target, device_id=device_id, wait_on_id=wait_on_id, token_duration=3600 ) diff --git a/azext_iot/tests/conftest.py b/azext_iot/tests/conftest.py index 47de77eeb..7dfb38ae0 100644 --- a/azext_iot/tests/conftest.py +++ b/azext_iot/tests/conftest.py @@ -57,7 +57,7 @@ def fixture_cmd2(mocker): def test_handler1(): pass - return AzCliCommand(cli.loader, 'iot-extension command', test_handler1) + return AzCliCommand(cli.loader, "iot-extension command", test_handler1) # Sets current working directory to the directory of the executing file @@ -74,12 +74,6 @@ def fixture_cmd(mocker): return cmd -@pytest.fixture() -def fixture_events_uamqp_sendclient(mocker): - from azext_iot.operations.events3._events import uamqp - return mocker.patch.object(uamqp, "SendClient", autospec=True) - - @pytest.fixture() def fixture_service_client_generic(mocker, fixture_ghcs, fixture_sas): service_client = mocker.patch(path_service_client) @@ -142,13 +136,17 @@ def fixture_monitor_events_entrypoint(mocker): # 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): +def build_mock_response( + mocker=None, status_code=200, payload=None, headers=None, **kwargs +): try: from unittest.mock import MagicMock except: from mock import MagicMock - response = mocker.MagicMock(name="response") if mocker else MagicMock(name="response") + response = ( + mocker.MagicMock(name="response") if mocker else MagicMock(name="response") + ) response.status_code = status_code del response.context del response._attribute_map @@ -161,8 +159,8 @@ def build_mock_response(mocker=None, status_code=200, payload=None, headers=None response.text = _payload_str response.internal_response.json.return_value = json.loads(_payload_str) else: - response.text.return_value = '' - response.text = '' + response.text.return_value = "" + response.text = "" headers_get_side_effect = kwargs.get("headers_get_side_effect") if headers_get_side_effect: @@ -192,13 +190,31 @@ def mocked_response(): @pytest.fixture(params=[400, 401, 500]) def service_client_generic_errors(mocked_response, fixture_ghcs, request): def error_callback(_): - return (request.param, {'Content-Type': "application/json; charset=utf-8"}, json.dumps({"error": "something failed"})) + return ( + request.param, + {"Content-Type": "application/json; charset=utf-8"}, + json.dumps({"error": "something failed"}), + ) any_endpoint = r"^https:\/\/.+" with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: - rsps.add_callback(callback=error_callback, method=responses.GET, url=re.compile(any_endpoint)) - rsps.add_callback(callback=error_callback, method=responses.PUT, url=re.compile(any_endpoint)) - rsps.add_callback(callback=error_callback, method=responses.POST, url=re.compile(any_endpoint)) - rsps.add_callback(callback=error_callback, method=responses.DELETE, url=re.compile(any_endpoint)) - rsps.add_callback(callback=error_callback, method=responses.PATCH, url=re.compile(any_endpoint)) + rsps.add_callback( + callback=error_callback, method=responses.GET, url=re.compile(any_endpoint) + ) + rsps.add_callback( + callback=error_callback, method=responses.PUT, url=re.compile(any_endpoint) + ) + rsps.add_callback( + callback=error_callback, method=responses.POST, url=re.compile(any_endpoint) + ) + rsps.add_callback( + callback=error_callback, + method=responses.DELETE, + url=re.compile(any_endpoint), + ) + rsps.add_callback( + callback=error_callback, + method=responses.PATCH, + url=re.compile(any_endpoint), + ) yield rsps diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index 46f2d4f4c..df69f8a52 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -21,7 +21,7 @@ ) from azext_iot.common.deps import ensure_uamqp from azext_iot.constants import EVENT_LIB, EXTENSION_NAME -from azext_iot.operations.events3 import _parser +from azext_iot.monitor.handlers import _parser from azext_iot._validators import mode2_iot_login_handler from .helpers import load_json from .test_constants import FileNames diff --git a/azext_iot/tests/test_uamqp_import.py b/azext_iot/tests/test_uamqp_import.py new file mode 100644 index 000000000..e66b24ccd --- /dev/null +++ b/azext_iot/tests/test_uamqp_import.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import mock + + +class TestUamqpImport(object): + def test_import_error(self): + """ + This test should throw if any module is importing uamqp + Add any new top level modules here to ensure they aren't importing uamqp + """ + with mock.patch.dict("sys.modules", {"uamqp": None}): + import azext_iot.assets + import azext_iot.central + import azext_iot.common + import azext_iot.dps + import azext_iot.iothub + import azext_iot.models + + assert azext_iot.assets + assert azext_iot.central + assert azext_iot.common + assert azext_iot.dps + assert azext_iot.iothub + assert azext_iot.models From eed07703815072240f666f11d2eac8acbfad20c9 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 7 May 2020 14:11:21 -0700 Subject: [PATCH 029/179] Update CODEOWNERS --- .github/CODEOWNERS | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f491da409..f8f0642c7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,11 @@ # This is a comment. # Each line is a file pattern followed by one or more owners. -* @digimaun \ No newline at end of file +# Global Code Owner(s) +* @digimaun + +# Central Code Owner(s) +azext_iot/central/ @prbans + +# Monitor Code Owner(s) +azext_iot/monitor/ @prbans @digimaun From eaefa9cbe1e5afcfb7140e8cb31f00fae5b87298 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 7 May 2020 14:32:20 -0700 Subject: [PATCH 030/179] Update _help.py (#180) --- azext_iot/central/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 53ddee895..773b207aa 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -189,7 +189,7 @@ def _load_central_device_templates_help(): type: command short-summary: Delete a device template from IoTC long-summary: | - Note: this is expected to fail if any devices are still registered to this template. + Note: this is expected to fail if any devices are still associated to this template. examples: - name: Delete a device template from IoTC From 5aa974c1b687be4526d7d8efbb252d27bd346421 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Wed, 13 May 2020 14:40:23 -0700 Subject: [PATCH 031/179] Central validate messages ux cleanup (#179) * Moved all central code to new pattern * Foundations done * Removed dead code, minor renames * Fixed failing tests * Added tests back in for Parsers * Updated references of central_api_uri to central_dns_suffix, fixed tests * Plumbed everything, added params * Minor update to output text * Fixed UT, give message its own field, parse device_id first, move away from excessive usage of kwargs * moved away from static methods * Remove pnp_context * Removed kwargs, removed reference to --simulate-errors * Fixed some documentation, updated output to once per 5 messages * time_range -> duration, output cleanup * Moved help to be in central help, identified deprecated help, change arg passing style * IoTC -> IoT Central * Update default behavior and error string * change new line placement * Addressed comments --- azext_iot/__init__.py | 2 - azext_iot/_help.py | 164 ------- azext_iot/_params.py | 79 ---- azext_iot/central/_help.py | 211 ++++++++- azext_iot/central/command_map.py | 60 +++ azext_iot/central/commands_device_twin.py | 38 ++ azext_iot/central/commands_monitor.py | 105 +++++ azext_iot/central/params.py | 114 ++++- .../central/providers/device_provider.py | 14 +- .../central/providers/monitor_provider.py | 90 ++++ azext_iot/commands.py | 44 -- azext_iot/common/_azure.py | 4 +- .../base_handler.py => base_classes.py} | 11 +- .../builders/central_target_builder.py | 8 +- azext_iot/monitor/handlers/__init__.py | 5 +- azext_iot/monitor/handlers/_internal.py | 66 --- azext_iot/monitor/handlers/_parser.py | 437 ------------------ azext_iot/monitor/handlers/central_handler.py | 157 +++++++ azext_iot/monitor/handlers/common_handler.py | 59 +++ azext_iot/monitor/handlers/handler.py | 54 --- azext_iot/monitor/models/arguments.py | 74 +++ azext_iot/monitor/models/enum.py | 14 + azext_iot/monitor/parsers/__init__.py | 5 + azext_iot/monitor/parsers/central_parser.py | 182 ++++++++ azext_iot/monitor/parsers/common_parser.py | 184 ++++++++ azext_iot/monitor/parsers/issue.py | 130 ++++++ azext_iot/monitor/parsers/strings.py | 116 +++++ azext_iot/monitor/telemetry.py | 6 +- azext_iot/monitor/utility.py | 21 +- azext_iot/operations/central.py | 143 ------ azext_iot/operations/digitaltwin.py | 341 ++++++++++---- azext_iot/operations/hub.py | 25 +- azext_iot/tests/test_iot_central_int.py | 2 +- azext_iot/tests/test_iot_central_unit.py | 9 +- azext_iot/tests/test_iot_digitaltwin_unit.py | 2 - azext_iot/tests/test_iot_ext_unit.py | 1 - azext_iot/tests/test_iot_utility_unit.py | 436 ----------------- azext_iot/tests/test_monitor_parsers_unit.py | 407 ++++++++++++++++ 38 files changed, 2235 insertions(+), 1585 deletions(-) create mode 100644 azext_iot/central/commands_device_twin.py create mode 100644 azext_iot/central/commands_monitor.py create mode 100644 azext_iot/central/providers/monitor_provider.py rename azext_iot/monitor/{handlers/base_handler.py => base_classes.py} (71%) delete mode 100644 azext_iot/monitor/handlers/_internal.py delete mode 100644 azext_iot/monitor/handlers/_parser.py create mode 100644 azext_iot/monitor/handlers/central_handler.py create mode 100644 azext_iot/monitor/handlers/common_handler.py delete mode 100644 azext_iot/monitor/handlers/handler.py create mode 100644 azext_iot/monitor/models/arguments.py create mode 100644 azext_iot/monitor/models/enum.py create mode 100644 azext_iot/monitor/parsers/__init__.py create mode 100644 azext_iot/monitor/parsers/central_parser.py create mode 100644 azext_iot/monitor/parsers/common_parser.py create mode 100644 azext_iot/monitor/parsers/issue.py create mode 100644 azext_iot/monitor/parsers/strings.py delete mode 100644 azext_iot/operations/central.py create mode 100644 azext_iot/tests/test_monitor_parsers_unit.py diff --git a/azext_iot/__init__.py b/azext_iot/__init__.py index e09d53fb4..25f7d809e 100644 --- a/azext_iot/__init__.py +++ b/azext_iot/__init__.py @@ -24,8 +24,6 @@ client_factory=iot_service_provisioning_factory, ) -iotcentral_ops = CliCommandType(operations_tmpl="azext_iot.operations.central#{}") - iotdigitaltwin_ops = CliCommandType( operations_tmpl="azext_iot.operations.digitaltwin#{}" ) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index c8ce24f1e..b688997ae 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -1115,170 +1115,6 @@ short-summary: Delete a device registration in an Azure IoT Hub Device Provisioning Service. """ -helps[ - "iotcentral app monitor-events" -] = """ - type: command - short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. - long-summary: | - EXPERIMENTAL requires Python 3.5+ - This command relies on and may install dependent Cython package (uamqp) upon first execution. - https://github.com/Azure/azure-uamqp-python - - DEPRECATED. Use 'az iot central app monitor-events' instead. - examples: - - name: Basic usage - text: > - az iotcentral app monitor-events --app-id {app_id} - - name: Basic usage when filtering on target device - text: > - az iotcentral app monitor-events --app-id {app_id} -d {device_id} - - name: Basic usage when filtering targeted devices with a wildcard in the ID - text: > - az iotcentral app monitor-events --app-id {app_id} -d Device* - - name: Filter device and specify an Event Hub consumer group to bind to. - text: > - az iotcentral app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} - - name: Receive message annotations (message headers) - text: > - az iotcentral app monitor-events --app-id {app_id} -d {device_id} --properties anno - - name: Receive message annotations + system properties. Never time out. - text: > - az iotcentral app monitor-events --app-id {app_id} -d {device_id} --properties anno sys --timeout 0 - - name: Receive all message attributes from all device messages - text: > - az iotcentral app monitor-events --app-id {app_id} --props all - - name: Receive all messages and parse message payload as JSON - text: > - az iotcentral app monitor-events --app-id {app_id} --output json - """ - -helps[ - "iotcentral device-twin" -] = """ - type: group - short-summary: Manage IoT Central device twins. - long-summary: DEPRECATED. Use 'az iot central device-twin' instead. -""" - -helps[ - "iotcentral device-twin show" -] = """ - type: command - short-summary: Get the device twin from IoT Hub. - long-summary: DEPRECATED. Use 'az iot central device-twin show' instead. -""" - -helps[ - "iot central" -] = """ - type: group - short-summary: Manage Azure IoT Central assets. -""" - -helps[ - "iot central app" -] = """ - type: group - short-summary: | - Manage Azure IoT Central applications. - - To use this command group, the user must be logged through the `az login` command, - have the correct tenant set (the users home tenant) and - have access to the application through http://apps.azureiotcentral.com" -""" - -helps[ - "iot central app monitor-events" -] = """ - type: command - short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. - long-summary: | - EXPERIMENTAL requires Python 3.5+ - This command relies on and may install dependent Cython package (uamqp) upon first execution. - https://github.com/Azure/azure-uamqp-python - examples: - - name: Basic usage - text: > - az iot central app monitor-events --app-id {app_id} - - name: Basic usage when filtering on target device - text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} - - name: Basic usage when filtering targeted devices with a wildcard in the ID - text: > - az iot central app monitor-events --app-id {app_id} -d Device* - - name: Filter device and specify an Event Hub consumer group to bind to. - text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} - - name: Receive message annotations (message headers) - text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno - - name: Receive message annotations + system properties. Never time out. - text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno sys --timeout 0 - - name: Receive all message attributes from all device messages - text: > - az iot central app monitor-events --app-id {app_id} --props all - - name: Receive all messages and parse message payload as JSON - text: > - az iot central app monitor-events --app-id {app_id} --output json - """ - - -helps[ - "iot central app validate-messages" -] = """ - type: command - short-summary: Validate messages sent to the IoT Hub for an IoT Central app. - long-summary: | - EXPERIMENTAL requires Python 3.5+ - This command relies on and may install dependent Cython package (uamqp) upon first execution. - https://github.com/Azure/azure-uamqp-python - examples: - - name: Basic usage - text: > - az iot central app validate-messages --app-id {app_id} - - name: Basic usage when filtering on target device - text: > - az iot central app validate-messages --app-id {app_id} -d {device_id} - - name: Basic usage when filtering targeted devices with a wildcard in the ID - text: > - az iot central app validate-messages --app-id {app_id} -d Device* - - name: Filter device and specify an Event Hub consumer group to bind to. - text: > - az iot central app validate-messages --app-id {app_id} -d {device_id} --cg {consumer_group_name} - - name: Will randomly convert a recieved message into an error so you can see what sample errors might look like. - text: > - az iot central app validate-messages --app-id {app_id} --simulate-errors - """ - -helps[ - "iot central device-twin" -] = """ - type: group - short-summary: Manage IoT Central device twins. -""" - -helps[ - "iot central device-twin show" -] = """ - type: command - short-summary: Get the device twin from IoT Hub. -""" - -helps[ - "iot central app device-twin" -] = """ - type: group - short-summary: Manage IoT Central device twins. -""" - -helps[ - "iot central app device-twin show" -] = """ - type: command - short-summary: Get the device twin from IoT Hub. -""" helps[ "iot dt" diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 3811523c2..10b73acba 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -834,85 +834,6 @@ def load_arguments(self, _): with self.argument_context("iot dps registration list") as context: context.argument("enrollment_id", help="ID of enrollment group") - # TODO: Remove 'iotcentral', non-conventional and no reuse of arguments. 'iot central' is the go forward. - with self.argument_context("iotcentral app monitor-events") as context: - context.argument( - "device_id", options_list=["--device-id", "-d"], help="Target Device." - ) - context.argument("app_id", options_list=["--app-id"], help="Target App.") - context.argument( - "timeout", - options_list=["--timeout", "--to", "-t"], - type=int, - help="Maximum seconds to maintain connection without receiving message. Use 0 for infinity. ", - ) - context.argument( - "consumer_group", - options_list=["--consumer-group", "--cg", "-c"], - help="Specify the consumer group to use when connecting to event hub endpoint.", - ) - context.argument( - "enqueued_time", - options_list=["--enqueued-time", "--et", "-e"], - type=int, - help="Indicates the time that should be used as a starting point to read messages from the partitions. " - "Units are milliseconds since unix epoch. " - 'If no time is indicated "now" is used.', - ) - context.argument("properties", arg_type=event_msg_prop_type) - context.argument( - "content_type", - options_list=["--content-type", "--ct"], - help="Specify the Content-Type of the message payload to automatically format the output to that type.", - ) - context.argument( - "repair", - options_list=["--repair", "-r"], - arg_type=get_three_state_flag(), - help="Reinstall uamqp dependency compatible with extension version. Default: false", - ) - context.argument( - "central_api_uri", - options_list=["--central-api-uri"], - help="IoT Central API override. For use with environments other than production.", - ) - context.argument( - "yes", - options_list=["--yes", "-y"], - arg_type=get_three_state_flag(), - help="Skip user prompts. Indicates acceptance of dependency installation (if required). " - "Used primarily for automation scenarios. Default: false", - ) - - with self.argument_context("iotcentral device-twin show") as context: - context.argument( - "device_id", options_list=["--device-id", "-d"], help="Target Device." - ) - context.argument("app_id", options_list=["--app-id"], help="Target App.") - context.argument( - "central_api_uri", - options_list=["--central-api-uri"], - help="IoT Central API override. For use with environments other than production.", - ) - - with self.argument_context("iot central") as context: - context.argument("app_id", options_list=["--app-id"], help="Target App.") - context.argument( - "central_api_uri", - options_list=["--central-api-uri"], - help="IoT Central API override. For use with environments other than production.", - ) - - with self.argument_context("iot central app") as context: - context.argument("properties", arg_type=event_msg_prop_type) - - with self.argument_context("iot central app validate-messages") as context: - context.argument( - "simulate_errors", - arg_type=get_three_state_flag(), - help="Randomly converts messages received into errors. Used primarily for testing purposes. Default: false", - ) - with self.argument_context("iot dt") as context: context.argument( "repo_login", diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 773b207aa..ef2db7516 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -12,11 +12,28 @@ def load_central_help(): "iot central" ] = """ type: group - short-summary: Manage Azure Central (IoTC) solutions & infrastructure + short-summary: Manage Azure Central (IoT Central) solutions & infrastructure """ + helps[ + "iot central app" + ] = """ + type: group + short-summary: | + Manage Azure IoT Central applications. + + To use this command group, the user must be logged through the `az login` command, + have the correct tenant set (the users home tenant) and + have access to the application through http://apps.azureiotcentral.com" + """ + _load_central_devices_help() _load_central_device_templates_help() + _load_central_device_twin_help() + _load_central_monitors_help() + + # TODO: Delete this by end of July 2020 + _load_central_deprecated_commands() def _load_central_devices_help(): @@ -24,14 +41,14 @@ def _load_central_devices_help(): "iot central app device" ] = """ type: group - short-summary: Manage and configure IoTC devices + short-summary: Manage and configure IoT Central devices """ helps[ "iot central app device create" ] = """ type: command - short-summary: Create a device in IoTC + short-summary: Create a device in IoT Central examples: - name: Create a device @@ -53,7 +70,7 @@ def _load_central_devices_help(): "iot central app device show" ] = """ type: command - short-summary: Get a device from IoTC + short-summary: Get a device from IoT Central examples: - name: Get a device @@ -67,10 +84,10 @@ def _load_central_devices_help(): "iot central app device list" ] = """ type: command - short-summary: List all devices in IoTC + short-summary: List all devices in IoT Central examples: - - name: List all devices in IoTC + - name: List all devices in IoT Central text: > az iot central app device list --app-id {appid} @@ -80,7 +97,7 @@ def _load_central_devices_help(): "iot central app device delete" ] = """ type: command - short-summary: Delete a device from IoTC + short-summary: Delete a device from IoT Central examples: - name: Delete a device @@ -94,7 +111,7 @@ def _load_central_devices_help(): "iot central app device registration-info" ] = """ type: command - short-summary: Get registration info on device(s) from IoTC + short-summary: Get registration info on device(s) from IoT Central long-summary: | Note: This command can take a significant amount of time to return if no device id is specified and your app contains a lot of devices @@ -118,14 +135,14 @@ def _load_central_device_templates_help(): "iot central app device-template" ] = """ type: group - short-summary: Manage and configure IoTC device templates + short-summary: Manage and configure IoT Central device templates """ helps[ "iot central app device-template create" ] = """ type: command - short-summary: Create a device template in IoTC + short-summary: Create a device template in IoT Central examples: - name: Create a device template with payload read from a file @@ -147,7 +164,7 @@ def _load_central_device_templates_help(): "iot central app device-template show" ] = """ type: command - short-summary: Get a device template from IoTC + short-summary: Get a device template from IoT Central examples: - name: Get a device template @@ -161,7 +178,7 @@ def _load_central_device_templates_help(): "iot central app device-template list" ] = """ type: command - short-summary: List all device templates in IoTC + short-summary: List all device templates in IoT Central examples: - name: List all device templates @@ -187,14 +204,180 @@ def _load_central_device_templates_help(): "iot central app device-template delete" ] = """ type: command - short-summary: Delete a device template from IoTC + short-summary: Delete a device template from IoT Central long-summary: | Note: this is expected to fail if any devices are still associated to this template. examples: - - name: Delete a device template from IoTC + - name: Delete a device template from IoT Central text: > az iot central app device-template delete --app-id {appid} --device-template-id {devicetemplateid} """ + + +def _load_central_device_twin_help(): + helps[ + "iot central app device-twin" + ] = """ + type: group + short-summary: Manage IoT Central device twins. + """ + + helps[ + "iot central app device-twin show" + ] = """ + type: command + short-summary: Get the device twin from IoT Hub. + """ + + +def _load_central_monitors_help(): + helps[ + "iot central app monitor-events" + ] = """ + type: command + short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. + long-summary: | + EXPERIMENTAL requires Python 3.5+ + This command relies on and may install dependent Cython package (uamqp) upon first execution. + https://github.com/Azure/azure-uamqp-python + examples: + - name: Basic usage + text: > + az iot central app monitor-events --app-id {app_id} + - name: Basic usage when filtering on target device + text: > + az iot central app monitor-events --app-id {app_id} -d {device_id} + - name: Basic usage when filtering targeted devices with a wildcard in the ID + text: > + az iot central app monitor-events --app-id {app_id} -d Device* + - name: Filter device and specify an Event Hub consumer group to bind to. + text: > + az iot central app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} + - name: Receive message annotations (message headers) + text: > + az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno + - name: Receive message annotations + system properties. Never time out. + text: > + az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno sys --timeout 0 + - name: Receive all message attributes from all device messages + text: > + az iot central app monitor-events --app-id {app_id} --props all + - name: Receive all messages and parse message payload as JSON + text: > + az iot central app monitor-events --app-id {app_id} --output json + """ + + helps[ + "iot central app validate-messages" + ] = """ + type: command + short-summary: Validate messages sent to the IoT Hub for an IoT Central app. + long-summary: | + EXPERIMENTAL requires Python 3.5+ + This command relies on and may install dependent Cython package (uamqp) upon first execution. + https://github.com/Azure/azure-uamqp-python + examples: + - name: Basic usage + text: > + az iot central app validate-messages --app-id {app_id} + - name: Output errors as they are detected + text: > + az iot central app validate-messages --app-id {app_id} --style scroll + - name: Basic usage when filtering on target device + text: > + az iot central app validate-messages --app-id {app_id} -d {device_id} + - name: Basic usage when filtering targeted devices with a wildcard in the ID + text: > + az iot central app validate-messages --app-id {app_id} -d Device* + - name: Filter device and specify an Event Hub consumer group to bind to. + text: > + az iot central app validate-messages --app-id {app_id} -d {device_id} --cg {consumer_group_name} + """ + + +# TODO: Delete this by July 2020 +def _load_central_deprecated_commands(): + helps[ + "iotcentral app monitor-events" + ] = """ + type: command + short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. + long-summary: | + EXPERIMENTAL requires Python 3.5+ + This command relies on and may install dependent Cython package (uamqp) upon first execution. + https://github.com/Azure/azure-uamqp-python + + DEPRECATED. Use 'az iot central app monitor-events' instead. + examples: + - name: Basic usage + text: > + az iotcentral app monitor-events --app-id {app_id} + - name: Basic usage when filtering on target device + text: > + az iotcentral app monitor-events --app-id {app_id} -d {device_id} + - name: Basic usage when filtering targeted devices with a wildcard in the ID + text: > + az iotcentral app monitor-events --app-id {app_id} -d Device* + - name: Filter device and specify an Event Hub consumer group to bind to. + text: > + az iotcentral app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} + - name: Receive message annotations (message headers) + text: > + az iotcentral app monitor-events --app-id {app_id} -d {device_id} --properties anno + - name: Receive message annotations + system properties. Never time out. + text: > + az iotcentral app monitor-events --app-id {app_id} -d {device_id} --properties anno sys --timeout 0 + - name: Receive all message attributes from all device messages + text: > + az iotcentral app monitor-events --app-id {app_id} --props all + - name: Receive all messages and parse message payload as JSON + text: > + az iotcentral app monitor-events --app-id {app_id} --output json + """ + + helps[ + "iotcentral device-twin" + ] = """ + type: group + short-summary: Manage IoT Central device twins. + long-summary: DEPRECATED. Use 'az iot central device-twin' instead. + """ + + helps[ + "iotcentral device-twin show" + ] = """ + type: command + short-summary: Get the device twin from IoT Hub. + long-summary: DEPRECATED. Use 'az iot central device-twin show' instead. + """ + + helps[ + "iot central device-twin" + ] = """ + type: group + short-summary: Manage IoT Central device twins. + """ + + helps[ + "iot central device-twin show" + ] = """ + type: command + short-summary: Get the device twin from IoT Hub. + """ + + helps[ + "iotcentral app device-twin" + ] = """ + type: group + short-summary: Manage IoT Central device twins. + """ + + helps[ + "iotcentral app device-twin show" + ] = """ + type: command + short-summary: Manage IoT Central device twins. + """ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index 996af1352..53e58d373 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -17,12 +17,26 @@ operations_tmpl="azext_iot.central.commands_device_template#{}" ) +central_device_twin_ops = CliCommandType( + operations_tmpl="azext_iot.central.commands_device_twin#{}" +) + +central_monitor_ops = CliCommandType( + operations_tmpl="azext_iot.central.commands_monitor#{}" +) + # Dev note - think of this as the "router" and all self.command_group as the controllers def load_central_commands(self, _): """ Load CLI commands """ + with self.command_group( + "iot central app", command_type=central_monitor_ops, + ) as cmd_group: + cmd_group.command("monitor-events", "monitor_events") + cmd_group.command("validate-messages", "validate_messages", is_preview=True) + with self.command_group( "iot central app device", command_type=central_device_ops, is_preview=True, ) as cmd_group: @@ -42,3 +56,49 @@ def load_central_commands(self, _): cmd_group.command("show", "get_device_template") cmd_group.command("create", "create_device_template") cmd_group.command("delete", "delete_device_template") + + with self.command_group( + "iot central app device-twin", command_type=central_device_twin_ops + ) as cmd_group: + cmd_group.command("show", "device_twin_show") + + # TODO: Delete this by end of July 2020 + load_deprecated_commands(self, _) + + +# TODO: Delete this by end of July 2020 +def load_deprecated_commands(self, _): + with self.command_group( + "iotcentral", + command_type=central_monitor_ops, + deprecate_info=self.deprecate(redirect="iot central"), + ) as _: + pass + + with self.command_group( + "iotcentral app", + command_type=central_monitor_ops, + deprecate_info=self.deprecate(redirect="iot central app"), + ) as cmd_group: + cmd_group.command("monitor-events", "monitor_events") + + with self.command_group( + "iotcentral device-twin", + command_type=central_device_twin_ops, + deprecate_info=self.deprecate(redirect="iot central app device-twin"), + ) as cmd_group: + cmd_group.command("show", "device_twin_show") + + with self.command_group( + "iotcentral app device-twin", + command_type=central_device_twin_ops, + deprecate_info=self.deprecate(redirect="iot central app device-twin"), + ) as cmd_group: + cmd_group.command("show", "device_twin_show") + + with self.command_group( + "iot central device-twin", + command_type=central_device_twin_ops, + deprecate_info=self.deprecate(redirect="iot central app device-twin"), + ) as cmd_group: + cmd_group.command("show", "device_twin_show") diff --git a/azext_iot/central/commands_device_twin.py b/azext_iot/central/commands_device_twin.py new file mode 100644 index 000000000..b61a575c4 --- /dev/null +++ b/azext_iot/central/commands_device_twin.py @@ -0,0 +1,38 @@ +# 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 +from azext_iot._factory import _bind_sdk +from azext_iot.common.shared import SdkType +from azext_iot.common.utility import unpack_msrest_error +from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication + + +def find_between(s, start, end): + return (s.split(start))[1].split(end)[0] + + +def device_twin_show(cmd, device_id, app_id, central_dns_suffix="azureiotcentral.com"): + from azext_iot.common._azure import get_iot_central_tokens + + tokens = get_iot_central_tokens(cmd, app_id, central_dns_suffix) + exception = None + + # The device could be in any hub associated with the given app. + # We must search through each IoT Hub until device is found. + for token_group in tokens.values(): + sas_token = token_group["iothubTenantSasToken"]["sasToken"] + endpoint = find_between(sas_token, "SharedAccessSignature sr=", "&sig=") + target = {"entity": endpoint} + auth = BasicSasTokenAuthentication(sas_token=sas_token) + service_sdk, errors = _bind_sdk(target, SdkType.service_sdk, auth=auth) + try: + return service_sdk.get_twin(device_id) + except errors.CloudError as e: + if exception is None: + exception = CLIError(unpack_msrest_error(e)) + + raise exception diff --git a/azext_iot/central/commands_monitor.py b/azext_iot/central/commands_monitor.py new file mode 100644 index 000000000..b33168ceb --- /dev/null +++ b/azext_iot/central/commands_monitor.py @@ -0,0 +1,105 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.commands import AzCliCommand +from azext_iot.central.providers.monitor_provider import MonitorProvider +from azext_iot.monitor.models.enum import Severity +from azext_iot.monitor.models.arguments import ( + CommonParserArguments, + CommonHandlerArguments, + CentralHandlerArguments, + TelemetryArguments, +) + + +def validate_messages( + cmd: AzCliCommand, + app_id, + device_id=None, + consumer_group="$Default", + timeout=300, + enqueued_time=None, + repair=False, + properties=None, + yes=False, + max_messages=10, + duration=300, + style="scroll", + minimum_severity=Severity.warning.name, + central_dns_suffix="azureiotcentral.com", +): + telemetry_args = TelemetryArguments( + cmd, + timeout=timeout, + properties=properties, + enqueued_time=enqueued_time, + repair=repair, + yes=yes, + ) + common_parser_args = CommonParserArguments(properties=telemetry_args.properties) + common_handler_args = CommonHandlerArguments( + output=telemetry_args.output, + common_parser_args=common_parser_args, + device_id=device_id, + ) + central_handler_args = CentralHandlerArguments( + duration=duration, + max_messages=max_messages, + style=style, + minimum_severity=Severity[minimum_severity], + common_handler_args=common_handler_args, + ) + provider = MonitorProvider( + cmd=cmd, + app_id=app_id, + consumer_group=consumer_group, + central_dns_suffix=central_dns_suffix, + central_handler_args=central_handler_args, + ) + provider.start_validate_messages(telemetry_args) + + +def monitor_events( + cmd: AzCliCommand, + app_id, + device_id=None, + consumer_group="$Default", + timeout=300, + enqueued_time=None, + repair=False, + properties=None, + yes=False, + central_dns_suffix="azureiotcentral.com", +): + telemetry_args = TelemetryArguments( + cmd, + timeout=timeout, + properties=properties, + enqueued_time=enqueued_time, + repair=repair, + yes=yes, + ) + common_parser_args = CommonParserArguments(properties=telemetry_args.properties) + common_handler_args = CommonHandlerArguments( + output=telemetry_args.output, + common_parser_args=common_parser_args, + device_id=device_id, + ) + central_handler_args = CentralHandlerArguments( + duration=0, + max_messages=0, + style="scroll", + minimum_severity=Severity.warning, + common_handler_args=common_handler_args, + ) + provider = MonitorProvider( + cmd=cmd, + app_id=app_id, + consumer_group=consumer_group, + central_dns_suffix=central_dns_suffix, + central_handler_args=central_handler_args, + ) + provider.start_monitor_events(telemetry_args) diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index e2e3d3da5..747552403 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -8,8 +8,25 @@ CLI parameter definitions. """ +from knack.arguments import CLIArgumentType, CaseInsensitiveList + from azure.cli.core.commands.parameters import get_three_state_flag, get_enum_type from azext_iot.central.models.enum import DeviceStatus +from azext_iot.monitor.models.enum import Severity +from azext_iot._params import event_msg_prop_type, event_timeout_type + +severity_type = CLIArgumentType( + options_list=["--minimum-severity"], + choices=CaseInsensitiveList([sev.name for sev in Severity]), + help="Minimum severity of issue required for reporting.", +) + +style_type = CLIArgumentType( + options_list=["--style"], + choices=CaseInsensitiveList(["scroll", "json", "csv"]), + help="Indicate output style" + "scroll = deliver errors as they arrive, json = summarize results as json, csv = summarize results as json", +) def load_central_arguments(self, _): @@ -17,10 +34,12 @@ def load_central_arguments(self, _): Load CLI Args for Knack parser """ with self.argument_context("iot central app") as context: + context.argument("app_id", options_list=["--app-id"], help="Target App.") + context.argument("minimum_severity", arg_type=severity_type) context.argument( "instance_of", options_list=["--instance-of"], - help="Central model id. Example: urn:ojpkindbz:modelDefinition:iild3tm_uo", + help="Central template id. Example: urn:ojpkindbz:modelDefinition:iild3tm_uo", ) context.argument( "device_name", @@ -57,13 +76,102 @@ def load_central_arguments(self, _): ) context.argument( "central_dns_suffix", - options_list=["--central-dns-suffix"], + options_list=["--central-dns-suffix", "--central-api-uri"], help="Central dns suffix. " "This enables running cli commands against non public/prod environments", ) context.argument( "device_status", - options_list=["--devicestatus", "--ds"], + options_list=["--device-status", "--ds"], arg_type=get_enum_type(DeviceStatus), help="Indicates filter option for device status", ) + + with self.argument_context("iot central app monitor-events") as context: + context.argument("timeout", arg_type=event_timeout_type) + context.argument("properties", arg_type=event_msg_prop_type) + + with self.argument_context("iot central app validate-messages") as context: + context.argument("timeout", arg_type=event_timeout_type) + context.argument("properties", arg_type=event_msg_prop_type) + context.argument("style", arg_type=style_type) + context.argument( + "duration", + options_list=["--duration", "--dr"], + type=int, + help="Maximum duration to receive messages from target device before terminating connection." + "Use 0 for infinity.", + ) + context.argument( + "max_messages", + options_list=["--max-messages", "--mm"], + type=int, + help="Maximum number of messages to recieve from target device before terminating connection." + "Use 0 for infinity.", + ) + + # TODO: Delete this by end of July 2020 + load_deprecated_iotcentral_params(self, _) + + +# TODO: Delete this by end of July 2020 +def load_deprecated_iotcentral_params(self, _): + with self.argument_context("iotcentral") as context: + context.argument("app_id", options_list=["--app-id"], help="Target App.") + context.argument("properties", arg_type=event_msg_prop_type) + context.argument("timeout", arg_type=event_timeout_type) + context.argument( + "device_id", options_list=["--device-id", "-d"], help="Target Device." + ) + context.argument( + "timeout", + options_list=["--timeout", "--to", "-t"], + type=int, + help="Maximum seconds to maintain connection. Use 0 for infinity. ", + ) + context.argument( + "consumer_group", + options_list=["--consumer-group", "--cg", "-c"], + help="Specify the consumer group to use when connecting to event hub endpoint.", + ) + context.argument( + "enqueued_time", + options_list=["--enqueued-time", "--et", "-e"], + type=int, + help="Indicates the time that should be used as a starting point to read messages from the partitions. " + "Units are milliseconds since unix epoch. " + 'If no time is indicated "now" is used.', + ) + context.argument( + "content_type", + options_list=["--content-type", "--ct"], + help="Specify the Content-Type of the message payload to automatically format the output to that type.", + ) + context.argument( + "repair", + options_list=["--repair", "-r"], + arg_type=get_three_state_flag(), + help="Reinstall uamqp dependency compatible with extension version. Default: false", + ) + context.argument( + "yes", + options_list=["--yes", "-y"], + arg_type=get_three_state_flag(), + help="Skip user prompts. Indicates acceptance of dependency installation (if required). " + "Used primarily for automation scenarios. Default: false", + ) + context.argument( + "central_dns_suffix", + options_list=["--central-dns-suffix", "--central-api-uri"], + help="Central dns suffix. " + "This enables running cli commands against non public/prod environments", + ) + + with self.argument_context("iot central device-twin") as context: + context.argument("app_id", options_list=["--app-id"], help="Target App.") + context.argument( + "central_dns_suffix", + options_list=["--central-dns-suffix", "--central-api-uri"], + help="Central dns suffix. " + "This enables running cli commands against non public/prod environments", + ) diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 58eea5805..27dc2558e 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -60,7 +60,7 @@ def get_device( def get_device_template_by_device_id( self, device_id, central_dns_suffix="azureiotcentral.com", - ): + ) -> dict: from azext_iot.central.providers import CentralDeviceTemplateProvider if not device_id: @@ -103,7 +103,7 @@ def create_device( raise CLIError("Device id must be specified.") if device_id in self._devices: - raise CLIError("Device already exists") + raise CLIError("Device already exists.") device = central_services.device.create_device( cmd=self._cmd, @@ -161,7 +161,7 @@ def get_device_credentials( if not credentials: raise CLIError( - "Could not find device credentials for device '{}'".format(device_id) + "Could not find device credentials for device '{}'.".format(device_id) ) # add to cache @@ -204,10 +204,10 @@ def get_device_registration_info( def dps_populate_essential_info(self, dps_info, device_status): error = { - "provisioned": "None", - "registered": "Device it not yet provisioned.", - "blocked": "Device is blocked by admin", - "unassociated": "Device does not have a valid template associated with it", + "provisioned": "None.", + "registered": "Device is not yet provisioned.", + "blocked": "Device is blocked by admin.", + "unassociated": "Device does not have a valid template associated with it.", } filtered_dps_info = { diff --git a/azext_iot/central/providers/monitor_provider.py b/azext_iot/central/providers/monitor_provider.py new file mode 100644 index 000000000..8ce9664c6 --- /dev/null +++ b/azext_iot/central/providers/monitor_provider.py @@ -0,0 +1,90 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from azure.cli.core.commands import AzCliCommand +from azext_iot.central.providers import CentralDeviceProvider + +from azext_iot.monitor.models.arguments import ( + CentralHandlerArguments, + TelemetryArguments, +) + + +class MonitorProvider: + """ + Provider for monitor events and validating messages + """ + + def __init__( + self, + cmd: AzCliCommand, + app_id: str, + consumer_group: str, + central_handler_args: CentralHandlerArguments, + central_dns_suffix: str, + ): + central_device_provider = CentralDeviceProvider(cmd, app_id) + self._targets = self._build_targets( + cmd=cmd, + app_id=app_id, + consumer_group=consumer_group, + central_dns_suffix=central_dns_suffix, + ) + self._handler = self._build_handler( + central_device_provider=central_device_provider, + central_handler_args=central_handler_args, + ) + + def start_monitor_events(self, telemetry_args: TelemetryArguments): + from azext_iot.monitor import telemetry + + telemetry.start_multiple_monitors( + targets=self._targets, + enqueued_time_utc=telemetry_args.enqueued_time, + on_start_string=self._handler.generate_startup_string("Monitoring"), + on_message_received=self._handler.parse_message, + timeout=telemetry_args.timeout, + ) + + def start_validate_messages(self, telemetry_args: TelemetryArguments): + from azext_iot.monitor import telemetry + + telemetry.start_multiple_monitors( + targets=self._targets, + enqueued_time_utc=telemetry_args.enqueued_time, + on_start_string=self._handler.generate_startup_string("Validating"), + on_message_received=self._handler.validate_message, + timeout=telemetry_args.timeout, + ) + + def _build_targets( + self, + cmd: AzCliCommand, + app_id: str, + consumer_group: str, + central_dns_suffix: str, + ): + from azext_iot.monitor.builders import central_target_builder + + targets = central_target_builder.build_central_event_hub_targets( + cmd, app_id, central_dns_suffix + ) + [target.add_consumer_group(consumer_group) for target in targets] + + return targets + + def _build_handler( + self, + central_device_provider: CentralDeviceProvider, + central_handler_args: CentralHandlerArguments, + ): + from azext_iot.monitor.handlers import CentralHandler + + return CentralHandler( + central_device_provider=central_device_provider, + central_handler_args=central_handler_args, + ) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 7ca9541c7..c68ac9b36 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -13,7 +13,6 @@ iotdps_ops, iotdigitaltwin_ops, iotpnp_ops, - iotcentral_ops, ) @@ -164,49 +163,6 @@ def load_command_table(self, _): cmd_group.command("show", "iot_dps_registration_get") cmd_group.command("delete", "iot_dps_registration_delete") - with self.command_group( - "iotcentral", - command_type=iotcentral_ops, - deprecate_info=self.deprecate(redirect="iot central", hide=True), - ) as cmd_group: - pass - - with self.command_group("iotcentral app", command_type=iotcentral_ops) as cmd_group: - cmd_group.command( - "monitor-events", "iot_central_monitor_events", - ) - - with self.command_group( - "iotcentral device-twin", command_type=iotcentral_ops - ) as cmd_group: - cmd_group.command( - "show", "iot_central_device_show", - ) - - with self.command_group( - "iot central app", command_type=iotcentral_ops - ) as cmd_group: - cmd_group.command("monitor-events", "iot_central_monitor_events") - cmd_group.command( - "validate-messages", "iot_central_validate_messages", is_preview=True - ) - - with self.command_group( - "iot central device-twin", command_type=iotcentral_ops - ) as cmd_group: - cmd_group.command( - "show", - "iot_central_device_show", - deprecate_info=self.deprecate( - redirect="iot central app device-twin", hide=True - ), - ) - - with self.command_group( - "iot central app device-twin", command_type=iotcentral_ops - ) as cmd_group: - cmd_group.command("show", "iot_central_device_show") - with self.command_group( "iot dt", command_type=iotdigitaltwin_ops, is_preview=True ) as cmd_group: diff --git a/azext_iot/common/_azure.py b/azext_iot/common/_azure.py index 1fe5540c2..0c5656c4e 100644 --- a/azext_iot/common/_azure.py +++ b/azext_iot/common/_azure.py @@ -246,7 +246,7 @@ def _find_iot_dps_from_list(all_dps, dps_name): return result -def get_iot_central_tokens(cmd, app_id, central_api_uri): +def get_iot_central_tokens(cmd, app_id, central_dns_suffix): import requests aad_token = get_aad_token(cmd, resource="https://apps.azureiotcentral.com")[ @@ -254,7 +254,7 @@ def get_iot_central_tokens(cmd, app_id, central_api_uri): ] url = "https://{}.{}/system/iothubs/generateSasTokens".format( - app_id, central_api_uri + app_id, central_dns_suffix ) response = requests.post( url, headers={"Authorization": "Bearer {}".format(aad_token)} diff --git a/azext_iot/monitor/handlers/base_handler.py b/azext_iot/monitor/base_classes.py similarity index 71% rename from azext_iot/monitor/handlers/base_handler.py rename to azext_iot/monitor/base_classes.py index 409697fb2..76182b56b 100644 --- a/azext_iot/monitor/handlers/base_handler.py +++ b/azext_iot/monitor/base_classes.py @@ -7,10 +7,19 @@ from abc import ABC, abstractmethod +class AbstractBaseParser(ABC): + def __init__(self): + super().__init__() + + @abstractmethod + def parse_message(self, message) -> dict: + raise NotImplementedError() + + class AbstractBaseEventsHandler(ABC): def __init__(self): super().__init__() @abstractmethod def parse_message(self, message): - pass + raise NotImplementedError() diff --git a/azext_iot/monitor/builders/central_target_builder.py b/azext_iot/monitor/builders/central_target_builder.py index 8d24c8a48..3f941b4b3 100644 --- a/azext_iot/monitor/builders/central_target_builder.py +++ b/azext_iot/monitor/builders/central_target_builder.py @@ -12,15 +12,15 @@ from azext_iot.monitor.builders._common import convert_token_to_target -def build_central_event_hub_targets(cmd, app_id, central_api_uri) -> List[Target]: +def build_central_event_hub_targets(cmd, app_id, central_dns_suffix) -> List[Target]: event_loop = asyncio.get_event_loop() return event_loop.run_until_complete( - _build_central_event_hub_targets_async(cmd, app_id, central_api_uri) + _build_central_event_hub_targets_async(cmd, app_id, central_dns_suffix) ) -async def _build_central_event_hub_targets_async(cmd, app_id, central_api_uri): - all_tokens = get_iot_central_tokens(cmd, app_id, central_api_uri) +async def _build_central_event_hub_targets_async(cmd, app_id, central_dns_suffix): + all_tokens = get_iot_central_tokens(cmd, app_id, central_dns_suffix) targets = [await convert_token_to_target(token) for token in all_tokens.values()] return targets diff --git a/azext_iot/monitor/handlers/__init__.py b/azext_iot/monitor/handlers/__init__.py index f8648d3e5..0b2bd36c5 100644 --- a/azext_iot/monitor/handlers/__init__.py +++ b/azext_iot/monitor/handlers/__init__.py @@ -4,6 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_iot.monitor.handlers.handler import CommonHandler +from azext_iot.monitor.handlers.common_handler import CommonHandler +from azext_iot.monitor.handlers.central_handler import CentralHandler -__all__ = ["CommonHandler"] +__all__ = ["CommonHandler", "CentralHandler"] diff --git a/azext_iot/monitor/handlers/_internal.py b/azext_iot/monitor/handlers/_internal.py deleted file mode 100644 index 714c12c8f..000000000 --- a/azext_iot/monitor/handlers/_internal.py +++ /dev/null @@ -1,66 +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. -# -------------------------------------------------------------------------------------------- - -import json -import re -import yaml - -from azext_iot.monitor.handlers._parser import Event3Parser - - -def parse_message( - msg, - device_id, - devices, - pnp_context, - interface_name, - content_type, - properties, - output, - validate_messages, - simulate_errors, - central_device_provider, -): - parser = Event3Parser() - origin_device_id = parser.parse_device_id(msg) - - if not _should_process_device(origin_device_id, device_id, devices): - return - - parsed_msg = parser.parse_message( - msg, - pnp_context, - interface_name, - properties, - content_type, - simulate_errors, - central_device_provider, - ) - - if output.lower() == "json": - dump = json.dumps(parsed_msg, indent=4) - else: - dump = yaml.safe_dump(parsed_msg, default_flow_style=False) - - if validate_messages: - parser.write_logs() - - if not validate_messages: - print(dump, flush=True) - - -def _should_process_device(origin_device_id, device_id, devices): - if device_id and device_id != origin_device_id: - if "*" in device_id or "?" in device_id: - regex = re.escape(device_id).replace("\\*", ".*").replace("\\?", ".") + "$" - if not re.match(regex, origin_device_id): - return False - else: - return False - if devices and origin_device_id not in devices: - return False - - return True diff --git a/azext_iot/monitor/handlers/_parser.py b/azext_iot/monitor/handlers/_parser.py deleted file mode 100644 index c01d845b0..000000000 --- a/azext_iot/monitor/handlers/_parser.py +++ /dev/null @@ -1,437 +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. -# -------------------------------------------------------------------------------------------- - -import json -import random -import re - -from knack.log import get_logger -from uamqp.message import Message - -from azext_iot.common.utility import parse_entity, unicode_binary_map, ISO8601Validator -from azext_iot.central.providers import CentralDeviceProvider - -SUPPORTED_ENCODINGS = ["utf-8"] -DEVICE_ID_IDENTIFIER = b"iothub-connection-device-id" -INTERFACE_NAME_IDENTIFIER = b"iothub-interface-name" -random.seed(0) - -ios_validator = ISO8601Validator() - - -class Event3Parser(object): - _logger = get_logger(__name__) - - def __init__(self, logger=None): - self._reset_issues() - if logger: - self._logger = logger - - def parse_message( - self, - message: Message, - pnp_context: bool, - interface_name: str, - properties: dict, - content_type_hint: str, - simulate_errors: bool, - central_device_provider: CentralDeviceProvider, - ) -> dict: - self._reset_issues() - create_encoding_error = False - create_custom_header_warning = False - create_payload_error = False - create_payload_name_error = False - - if not properties: - properties = {} # guard against None being passed in - - i = random.randint(1, 4) - if simulate_errors and i == 1: - create_encoding_error = True - if simulate_errors and i == 2: - create_custom_header_warning = True - if simulate_errors and i == 3: - create_payload_error = True - if simulate_errors and i == 4: - create_payload_name_error = True - - system_properties = self._parse_system_properties(message) - - self._parse_content_encoding(message, system_properties, create_encoding_error) - - event = {} - - origin_device_id = self.parse_device_id(message) - event["origin"] = origin_device_id - - content_type = self._parse_content_type( - content_type_hint, - system_properties, - origin_device_id, - create_custom_header_warning, - ) - - if pnp_context: - message_interface_name = self._parse_interface_name( - message, pnp_context, interface_name, origin_device_id - ) - - event["interface"] = message_interface_name - - if properties: - event["properties"] = {} - - if "anno" in properties or "all" in properties: - annotations = self._parse_annotations(message) - event["annotations"] = annotations - - if system_properties and ("sys" in properties or "all" in properties): - event["properties"]["system"] = system_properties - - if "app" in properties or "all" in properties: - application_properties = self._parse_application_properties(message) - event["properties"]["application"] = application_properties - - payload = self._parse_payload( - message, origin_device_id, content_type, create_payload_error - ) - - self._perform_static_validations( - origin_device_id=origin_device_id, payload=payload - ) - - self._perform_dynamic_validations( - origin_device_id=origin_device_id, - payload=payload, - central_device_provider=central_device_provider, - create_payload_name_error=create_payload_name_error, - ) - - event["payload"] = payload - - event_source = {"event": event} - - return event_source - - def parse_device_id(self, message: Message) -> str: - try: - return str(message.annotations.get(DEVICE_ID_IDENTIFIER), "utf8") - except Exception: - self._errors.append("Device id not found in message: {}".format(message)) - - def write_logs(self) -> None: - for error in self._errors: - self._logger.error("[Error] " + error) - - for warning in self._warnings: - self._logger.warn("[Warning] " + warning) - - for info in self._info: - self._logger.info("[Info] " + info) - - def _reset_issues(self) -> None: - self._info = [] - self._warnings = [] - self._errors = [] - - def _parse_interface_name( - self, message: Message, pnp_context, interface_name, origin_device_id - ) -> str: - message_interface_name = "" - - try: - message_interface_name = str( - message.annotations.get(INTERFACE_NAME_IDENTIFIER), "utf8" - ) - except Exception: - self._errors.append( - "Unable to parse interface_name given a pnp_device. {}. " - "message: {}".format(origin_device_id, message) - ) - - if interface_name != message_interface_name: - self._errors.append( - "Inteface name mismatch. {}. " - "Expected: {}, Actual: {}".format( - origin_device_id, interface_name, message_interface_name - ) - ) - - return message_interface_name - - def _parse_system_properties(self, message: Message): - try: - return unicode_binary_map(parse_entity(message.properties, True)) - except Exception: - self._errors.append( - "Failed to parse system_properties for message {}.".format(message) - ) - return {} - - def _parse_content_encoding( - self, message: Message, system_properties, create_encoding_error - ) -> str: - content_encoding = "" - - if "content_encoding" in system_properties: - content_encoding = system_properties["content_encoding"] - - if not content_encoding: - self._errors.append("No encoding found for message: {}".format(message)) - return None - - if create_encoding_error: - content_encoding = "Some Random Encoding" - - if "utf-8" not in content_encoding.lower(): - self._errors.append( - "Unsupported encoding detected: '{}'. " - "The currently supported encodings are: {}. " - "System_properties: {}.".format( - content_encoding, SUPPORTED_ENCODINGS, system_properties - ) - ) - return None - - return content_encoding - - def _parse_content_type( - self, - content_type_hint, - system_properties, - origin_device_id, - create_custom_header_warning, - ) -> str: - content_type = "" - if content_type_hint: - content_type = content_type_hint - elif "content_type" in system_properties: - content_type = system_properties["content_type"] - - if create_custom_header_warning: - content_type = "Some Random Custom Header" - - if not content_type: - self._warnings.append( - "Content type not found in system_properties. " - "System_properties: {}".format(system_properties) - ) - - return content_type - - def _parse_annotations(self, message: Message): - try: - return unicode_binary_map(message.annotations) - except Exception: - self._warnings.append( - "Unable to decode message.annotations: {}".format(message.annotations) - ) - - def _parse_application_properties(self, message: Message): - try: - return unicode_binary_map(message.application_properties) - except Exception: - self._warnings.append( - "Unable to decode message.application_properties: {}".format( - message.application_properties - ) - ) - - def _parse_payload( - self, message: Message, origin_device_id, content_type, create_payload_error - ): - payload = "" - data = message.get_data() - - if data: - payload = str(next(data), "utf8") - - if create_payload_error: - payload = "Some Random Payload" - - if "application/json" not in content_type.lower(): - self._warnings.append( - "Content type not supported. " - "Content type found: {}. " - "Content type expected: application/json. " - "DeviceId: {}".format(content_type, origin_device_id) - ) - else: - try: - payload = json.loads( - re.compile(r"(\\r\\n)+|\\r+|\\n+").sub("", payload) - ) - except Exception: - self._errors.append( - "Invalid JSON format. " - "DeviceId: {}, Raw payload {}".format(origin_device_id, payload) - ) - - return payload - - # Static validations should only need information present in the payload - # i.e. there should be no need for network calls - def _perform_static_validations(self, origin_device_id: str, payload: dict): - # if its not a dictionary, something else went wrong with parsing - if not isinstance(payload, dict): - return - - self._validate_field_names(origin_device_id=origin_device_id, payload=payload) - - def _validate_field_names(self, origin_device_id: str, payload: dict): - # source: - # https://github.com/Azure/IoTPlugandPlay/tree/master/DTDL - regex = "^[a-zA-Z_][a-zA-Z0-9_]*$" - - # if a field name does not match the above regex, it is an invalid field name - invalid_field_names = [ - field_name - for field_name in payload.keys() - if not re.search(regex, field_name) - ] - if invalid_field_names: - self._errors.append( - "The following field names are not allowed: '{}'. " - "Payload: '{}'. " - "Message origin: '{}'.".format( - invalid_field_names, payload, origin_device_id - ) - ) - - # Dynamic validations should need data external to the payload - # e.g. device template - def _perform_dynamic_validations( - self, - origin_device_id: str, - payload: dict, - central_device_provider: CentralDeviceProvider, - create_payload_name_error=False, - ): - # if the payload is not a dictionary some other parsing error occurred - if not isinstance(payload, dict): - return - - # device provider was not passed in, no way to get the device template - if not isinstance(central_device_provider, CentralDeviceProvider): - return - - template = self._get_device_template( - origin_device_id=origin_device_id, - central_device_provider=central_device_provider, - ) - - # _get_device_template should log error if there was an issue - if not template: - return - - template_schemas = self._extract_template_schemas_from_template( - origin_device_id=origin_device_id, template=template - ) - - # _extract_template_schemas_from_template should log error if there was an issue - if not isinstance(template_schemas, dict): - return - - self._validate_payload_against_schema( - origin_device_id=origin_device_id, - payload=payload, - template_schemas=template_schemas, - ) - - def _get_device_template( - self, origin_device_id: str, central_device_provider: CentralDeviceProvider, - ): - try: - return central_device_provider.get_device_template_by_device_id( - origin_device_id - ) - except Exception as e: - self._errors.append( - "Unable to retrieve template for device: {}." - "Inner exception: {}".format(origin_device_id, e) - ) - - def _extract_template_schemas_from_template( - self, origin_device_id: str, template: dict - ): - try: - schemas = [] - dcm = template["capabilityModel"] - implements = dcm["implements"] - for implementation in implements: - contents = implementation["schema"]["contents"] - schemas.extend(contents) - return {schema["name"]: schema for schema in schemas} - except Exception: - self._errors.append( - "Unable to extract device schema for device: {}." - "Template: {}".format(origin_device_id, template) - ) - - # 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_against_schema( - self, origin_device_id: str, payload: dict, template_schemas: dict, - ): - template_schema_names = template_schemas.keys() - for name, value in payload.items(): - schema = template_schemas.get(name) - if not schema: - self._errors.append( - "Telemetry item '{}' is not present in capability model. " - "Device ID: {}. " - "List of allowed telemetry values for this type of device: {}. " - "NOTE: telemetry names are CASE-SENSITIVE".format( - name, origin_device_id, template_schema_names - ) - ) - - is_dict = isinstance(schema, dict) - if is_dict and not self._validate_types_match(value, schema): - expected_type = str(schema.get("schema")) - self._errors.append( - "Type mismatch. " - "Expected type: '{}'. " - "Value received: '{}'. " - "Telemetry identifier: {}. " - "Device ID: {}. " - "All dates/times/datetimes/durations must be ISO 8601 compliant.".format( - expected_type, value, name, origin_device_id - ) - ) - - def _validate_types_match(self, value, schema: dict) -> bool: - # suppress error if there is no "schema" in schema - # means something else went wrong - schema_type = schema.get("schema") - if not schema_type: - return True - - if schema_type == "boolean": - return isinstance(value, bool) - elif schema_type == "double": - return isinstance(value, (float, int)) - elif schema_type == "float": - return isinstance(value, (float, int)) - elif schema_type == "integer": - return isinstance(value, int) - elif schema_type == "long": - return isinstance(value, (float, int)) - elif schema_type == "string": - return isinstance(value, str) - elif schema_type == "time": - return ios_validator.is_iso8601_time(value) - elif schema_type == "date": - return ios_validator.is_iso8601_date(value) - elif schema_type == "dateTime": - return ios_validator.is_iso8601_datetime(value) - elif schema_type == "duration": - return ios_validator.is_iso8601_duration(value) - - # if the schema_type is not found above, suppress error - return True diff --git a/azext_iot/monitor/handlers/central_handler.py b/azext_iot/monitor/handlers/central_handler.py new file mode 100644 index 000000000..a46bc558d --- /dev/null +++ b/azext_iot/monitor/handlers/central_handler.py @@ -0,0 +1,157 @@ +# 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.monitor.utility import stop_monitor, get_loop +from azext_iot.central.providers.device_provider import CentralDeviceProvider +from azext_iot.monitor.handlers.common_handler import CommonHandler +from azext_iot.monitor.models.arguments import CentralHandlerArguments +from azext_iot.monitor.parsers.central_parser import CentralParser +from azext_iot.monitor.parsers.issue import Issue + + +class CentralHandler(CommonHandler): + def __init__( + self, + central_device_provider: CentralDeviceProvider, + central_handler_args: CentralHandlerArguments, + ): + super(CentralHandler, self).__init__( + common_handler_args=central_handler_args.common_handler_args + ) + + self._central_device_provider = central_device_provider + + self._central_handler_args = central_handler_args + + self._messages = [] + self._issues: List[Issue] = [] + + if self._central_handler_args.duration: + loop = get_loop() + # the monitor takes a few seconds to start + loop.call_later( + self._central_handler_args.duration + 5, self._quit_duration_exceeded + ) + + def validate_message(self, message): + parser = CentralParser( + message=message, + common_parser_args=self._common_handler_args.common_parser_args, + central_device_provider=self._central_device_provider, + ) + + if not self._should_process_device(parser.device_id): + return + + parsed_message = parser.parse_message() + + self._messages.append(parsed_message) + n_messages = len(self._messages) + + issues = parser.issues_handler.get_issues_with_minimum_severity( + self._central_handler_args.minimum_severity + ) + + self._issues.extend(issues) + + self._print_progress_update(n_messages) + + if ( + self._central_handler_args.max_messages + and n_messages >= self._central_handler_args.max_messages + ): + self._quit_messages_exceeded() + + if self._central_handler_args.style == "scroll" and issues: + [issue.log() for issue in issues] + + def generate_startup_string(self, name: str): + device_filter_text = "" + device_id = self._central_handler_args.common_handler_args.device_id + if device_id: + device_filter_text = ".\nFiltering on device: {}".format(device_id) + + exit_text = "" + if ( + self._central_handler_args.duration + and self._central_handler_args.max_messages + ): + exit_text = ".\nExiting after {} second(s), or {} message(s) have been parsed (whichever happens first).".format( + self._central_handler_args.duration, + self._central_handler_args.max_messages, + ) + elif self._central_handler_args.duration: + exit_text = ".\nExiting after {} second(s).".format( + self._central_handler_args.duration + ) + elif self._central_handler_args.max_messages: + exit_text = ".\nExiting after parsing {} message(s).".format( + self._central_handler_args.max_messages + ) + + result = "{} telemetry{}{}".format(name, device_filter_text, exit_text) + + return result + + def _print_progress_update(self, n_messages: int): + if (n_messages % self._central_handler_args.progress_interval) == 0: + print("Processed {} messages...".format(n_messages), flush=True) + + def _print_results(self): + n_messages = len(self._messages) + + if not self._issues: + print("No errors detected after parsing {} message(s).".format(n_messages)) + return + + if self._central_handler_args.style.lower() == "scroll": + return + + print("Processing and displaying results.") + + issues = [issue.json_repr() for issue in self._issues] + + if self._central_handler_args.style.lower() == "json": + self._handle_json_summary(issues) + return + + if self._central_handler_args.style.lower() == "csv": + self._handle_csv_summary(issues) + return + + def _handle_json_summary(self, issues: List[Issue]): + import json + + output = json.dumps(issues, indent=4) + print(output) + + def _handle_csv_summary(self, issues: List[Issue]): + import csv + import sys + + fieldnames = ["severity", "details", "message", "device_id", "template_id"] + writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames) + writer.writeheader() + for issue in issues: + writer.writerow(issue) + + def _quit_messages_exceeded(self): + message = "Successfully parsed {} message(s).".format( + self._central_handler_args.max_messages + ) + print(message, flush=True) + self._print_results() + stop_monitor() + + def _quit_duration_exceeded(self): + message = "{} second(s) have elapsed.".format( + self._central_handler_args.duration + ) + print(message, flush=True) + self._print_results() + stop_monitor() diff --git a/azext_iot/monitor/handlers/common_handler.py b/azext_iot/monitor/handlers/common_handler.py new file mode 100644 index 000000000..0e083c6d0 --- /dev/null +++ b/azext_iot/monitor/handlers/common_handler.py @@ -0,0 +1,59 @@ +# 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 re +import yaml + +from azext_iot.monitor.base_classes import AbstractBaseEventsHandler +from azext_iot.monitor.parsers.common_parser import CommonParser +from azext_iot.monitor.models.arguments import CommonHandlerArguments + + +class CommonHandler(AbstractBaseEventsHandler): + def __init__(self, common_handler_args: CommonHandlerArguments): + super(CommonHandler, self).__init__() + self._common_handler_args = common_handler_args + + def parse_message(self, message): + parser = CommonParser( + message=message, + common_parser_args=self._common_handler_args.common_parser_args, + ) + + if not self._should_process_device(parser.device_id): + return + + result = parser.parse_message() + + if self._common_handler_args.output.lower() == "json": + dump = json.dumps(result, indent=4) + else: + dump = yaml.safe_dump(result, default_flow_style=False) + + print(dump, flush=True) + + def _should_process_device(self, device_id): + expected_device_id = self._common_handler_args.device_id + expected_devices = self._common_handler_args.devices + + if expected_device_id and expected_device_id != device_id: + if "*" in expected_device_id or "?" in expected_device_id: + regex = ( + re.escape(expected_device_id) + .replace("\\*", ".*") + .replace("\\?", ".") + + "$" + ) + if not re.match(regex, device_id): + return False + else: + return False + + if expected_devices and device_id not in expected_devices: + return False + + return True diff --git a/azext_iot/monitor/handlers/handler.py b/azext_iot/monitor/handlers/handler.py deleted file mode 100644 index 57df4bf72..000000000 --- a/azext_iot/monitor/handlers/handler.py +++ /dev/null @@ -1,54 +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.monitor.handlers import _internal -from azext_iot.monitor.handlers.base_handler import AbstractBaseEventsHandler - -""" -Use this handler if you aren't sure which handler is right for you -""" - - -class CommonHandler(AbstractBaseEventsHandler): - def __init__( - self, - device_id=None, - devices=None, - pnp_context=None, - interface_name=None, - content_type=None, - properties=None, - output=None, - validate_messages=None, - simulate_errors=None, - central_device_provider=None, - ): - self.device_id = device_id - self.devices = devices - self.pnp_context = pnp_context - self.interface_name = interface_name - self.content_type = content_type - self.properties = properties - self.output = output - self.validate_messages = validate_messages - self.simulate_errors = simulate_errors - self.central_device_provider = central_device_provider - pass - - def parse_message(self, message): - _internal.parse_message( - message, - device_id=self.device_id, - devices=self.devices, - pnp_context=self.pnp_context, - interface_name=self.interface_name, - content_type=self.content_type, - properties=self.properties, - output=self.output, - validate_messages=self.validate_messages, - simulate_errors=self.simulate_errors, - central_device_provider=self.central_device_provider, - ) diff --git a/azext_iot/monitor/models/arguments.py b/azext_iot/monitor/models/arguments.py new file mode 100644 index 000000000..ffac7a448 --- /dev/null +++ b/azext_iot/monitor/models/arguments.py @@ -0,0 +1,74 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.commands import AzCliCommand +from azext_iot.common.utility import init_monitoring +from azext_iot.monitor.models.enum import Severity + + +class TelemetryArguments: + def __init__( + self, + cmd: AzCliCommand, + timeout: int, + properties: list, + enqueued_time: int, + repair: bool, + yes: bool, + ): + (enqueued_time, unique_properties, timeout_ms, output) = init_monitoring( + cmd=cmd, + timeout=timeout, + properties=properties, + enqueued_time=enqueued_time, + repair=repair, + yes=yes, + ) + self.output = output + self.timeout = timeout_ms + self.properties = unique_properties + self.enqueued_time = enqueued_time + + +class CommonParserArguments: + def __init__( + self, properties: list = None, interface_name="", content_type="", + ): + self.properties = properties or [] + self.interface_name = interface_name or "" + self.content_type = content_type or "" + + +class CommonHandlerArguments: + def __init__( + self, + output: str, + common_parser_args: CommonParserArguments, + devices: list = None, + device_id="", + ): + self.output = output + self.devices = devices or [] + self.device_id = device_id or "" + self.common_parser_args = common_parser_args + + +class CentralHandlerArguments: + def __init__( + self, + duration: int, + max_messages: int, + common_handler_args: CommonHandlerArguments, + style="json", + minimum_severity=Severity.warning, + progress_interval=5, + ): + self.duration = duration + self.max_messages = max_messages + self.minimum_severity = minimum_severity + self.progress_interval = progress_interval + self.style = style + self.common_handler_args = common_handler_args diff --git a/azext_iot/monitor/models/enum.py b/azext_iot/monitor/models/enum.py new file mode 100644 index 000000000..691b916e6 --- /dev/null +++ b/azext_iot/monitor/models/enum.py @@ -0,0 +1,14 @@ +# 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 enum import IntEnum + + +class Severity(IntEnum): + info = 1 + warning = 2 + error = 3 diff --git a/azext_iot/monitor/parsers/__init__.py b/azext_iot/monitor/parsers/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/monitor/parsers/__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/monitor/parsers/central_parser.py b/azext_iot/monitor/parsers/central_parser.py new file mode 100644 index 000000000..76bb01512 --- /dev/null +++ b/azext_iot/monitor/parsers/central_parser.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. +# -------------------------------------------------------------------------------------------- + +import re + +from uamqp.message import Message + +from azext_iot.common.utility import ISO8601Validator +from azext_iot.central.providers import CentralDeviceProvider +from azext_iot.monitor.parsers import strings +from azext_iot.monitor.models.arguments import CommonParserArguments +from azext_iot.monitor.models.enum import Severity +from azext_iot.monitor.parsers.common_parser import CommonParser + +ios_validator = ISO8601Validator() + + +class CentralParser(CommonParser): + def __init__( + self, + message: Message, + common_parser_args: CommonParserArguments, + central_device_provider: CentralDeviceProvider, + ): + super(CentralParser, self).__init__( + message=message, common_parser_args=common_parser_args + ) + self._central_device_provider = central_device_provider + self._template_id = None + + def _add_central_issue(self, severity: Severity, details: str): + self.issues_handler.add_central_issue( + severity=severity, + details=details, + message=self._message, + device_id=self.device_id, + template_id=self._template_id, + ) + + def parse_message(self) -> dict: + parsed_message = super(CentralParser, self).parse_message() + + payload = parsed_message["event"]["payload"] + + self._perform_static_validations(payload=payload) + + self._perform_dynamic_validations(payload=payload) + + return parsed_message + + # Static validations should only need information present in the payload + # i.e. there should be no need for network calls + def _perform_static_validations(self, payload: dict): + # if its not a dictionary, something else went wrong with parsing + if not isinstance(payload, dict): + return + + self._validate_field_names(payload=payload) + + def _validate_field_names(self, payload: dict): + # source: + # https://github.com/Azure/IoTPlugandPlay/tree/master/DTDL + regex = "^[a-zA-Z_][a-zA-Z0-9_]*$" + + # if a field name does not match the above regex, it is an invalid field name + invalid_field_names = [ + field_name + for field_name in payload.keys() + if not re.search(regex, field_name) + ] + if invalid_field_names: + details = strings.invalid_field_name(invalid_field_names) + self._add_issue(severity=Severity.warning, details=details) + + # Dynamic validations should need data external to the payload + # e.g. device template + def _perform_dynamic_validations(self, payload: dict): + # if the payload is not a dictionary some other parsing error occurred + if not isinstance(payload, dict): + return + + template = self._get_device_template() + + # _get_device_template should log error if there was an issue + if not template: + return + + template_schemas = self._extract_template_schemas_from_template( + template=template + ) + + # _extract_template_schemas_from_template should log error if there was an issue + if not isinstance(template_schemas, dict): + return + + self._validate_payload_against_schema( + payload=payload, template_schemas=template_schemas, + ) + + def _get_device_template(self): + try: + return self._central_device_provider.get_device_template_by_device_id( + self.device_id + ) + except Exception as e: + details = strings.device_template_not_found(e) + self._add_central_issue(severity=Severity.error, details=details) + + def _extract_template_schemas_from_template(self, template: dict): + try: + self._template_id = template.get("id") + schemas = [] + dcm = template["capabilityModel"] + implements = dcm["implements"] + for implementation in implements: + contents = implementation["schema"]["contents"] + schemas.extend(contents) + return {schema["name"]: schema for schema in schemas} + except Exception: + details = strings.invalid_template_extract_schema_failed(template) + self._add_central_issue(severity=Severity.error, details=details) + + # 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_against_schema( + self, payload: dict, template_schemas: dict, + ): + template_schema_names = template_schemas.keys() + name_miss = [] + for name, value in payload.items(): + schema = template_schemas.get(name) + if not schema: + name_miss.append(name) + + is_dict = isinstance(schema, dict) + if is_dict and not self._validate_types_match(value, schema): + expected_type = str(schema.get("schema")) + details = strings.invalid_primitive_schema_mismatch_template( + name, expected_type, value + ) + self._add_central_issue(severity=Severity.warning, details=details) + + if name_miss: + details = strings.invalid_field_name_mismatch_template( + name_miss, list(template_schema_names) + ) + self._add_central_issue(severity=Severity.warning, details=details) + + def _validate_types_match(self, value, schema: dict) -> bool: + # suppress error if there is no "schema" in schema + # means something else went wrong + schema_type = schema.get("schema") + if not schema_type: + return True + + if schema_type == "boolean": + return isinstance(value, bool) + elif schema_type == "double": + return isinstance(value, (float, int)) + elif schema_type == "float": + return isinstance(value, (float, int)) + elif schema_type == "integer": + return isinstance(value, int) + elif schema_type == "long": + return isinstance(value, (float, int)) + elif schema_type == "string": + return isinstance(value, str) + elif schema_type == "time": + return ios_validator.is_iso8601_time(value) + elif schema_type == "date": + return ios_validator.is_iso8601_date(value) + elif schema_type == "dateTime": + return ios_validator.is_iso8601_datetime(value) + elif schema_type == "duration": + return ios_validator.is_iso8601_duration(value) + + # if the schema_type is not found above, suppress error + return True diff --git a/azext_iot/monitor/parsers/common_parser.py b/azext_iot/monitor/parsers/common_parser.py new file mode 100644 index 000000000..0ebd3c732 --- /dev/null +++ b/azext_iot/monitor/parsers/common_parser.py @@ -0,0 +1,184 @@ +# 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 re + +from uamqp.message import Message + +from azext_iot.common.utility import parse_entity, unicode_binary_map +from azext_iot.monitor.base_classes import AbstractBaseParser +from azext_iot.monitor.parsers import strings +from azext_iot.monitor.models.arguments import CommonParserArguments +from azext_iot.monitor.models.enum import Severity +from azext_iot.monitor.parsers.issue import IssueHandler + +DEVICE_ID_IDENTIFIER = b"iothub-connection-device-id" +INTERFACE_NAME_IDENTIFIER = b"iothub-interface-name" + + +class CommonParser(AbstractBaseParser): + def __init__(self, message: Message, common_parser_args: CommonParserArguments): + self.issues_handler = IssueHandler() + self._common_parser_args = common_parser_args + self._message = message + self.device_id = self._parse_device_id(message) + + def parse_message(self) -> dict: + message = self._message + properties = self._common_parser_args.properties + content_type = self._common_parser_args.content_type + interface_name = self._common_parser_args.interface_name + + event = {} + event["origin"] = self.device_id + + if not properties: + properties = [] # guard against None being passed in + + system_properties = self._parse_system_properties(message) + + self._parse_content_encoding(message, system_properties) + + content_type = self._parse_content_type(content_type, system_properties) + + if interface_name: + message_interface_name = self._parse_interface_name(message, interface_name) + + event["interface"] = message_interface_name + + if properties: + event["properties"] = {} + + if "anno" in properties or "all" in properties: + annotations = self._parse_annotations(message) + event["annotations"] = annotations + + if system_properties and ("sys" in properties or "all" in properties): + event["properties"]["system"] = system_properties + + if "app" in properties or "all" in properties: + application_properties = self._parse_application_properties(message) + event["properties"]["application"] = application_properties + + payload = self._parse_payload(message, content_type) + + event["payload"] = payload + + event_source = {"event": event} + + return event_source + + def _add_issue(self, severity: Severity, details: str): + self.issues_handler.add_issue( + severity=severity, + details=details, + message=self._message, + device_id=self.device_id, + ) + + def _parse_device_id(self, message: Message) -> str: + try: + return str(message.annotations.get(DEVICE_ID_IDENTIFIER), "utf8") + except Exception: + details = strings.unknown_device_id() + self._add_issue(severity=Severity.error, details=details) + return "" + + def _parse_interface_name(self, message: Message, expected_interface_name) -> str: + actual_interface_name = "" + + try: + actual_interface_name = str( + message.annotations.get(INTERFACE_NAME_IDENTIFIER), "utf8" + ) + except Exception: + details = strings.invalid_interface_name_not_found() + self._add_issue(severity=Severity.error, details=details) + + if expected_interface_name != actual_interface_name: + details = strings.invalid_interface_name_mismatch( + expected_interface_name, actual_interface_name + ) + self._add_issue(severity=Severity.warning, details=details) + + return actual_interface_name + + def _parse_system_properties(self, message: Message): + try: + return unicode_binary_map(parse_entity(message.properties, True)) + except Exception: + details = strings.invalid_system_properties() + self._add_issue(severity=Severity.error, details=details) + return {} + + def _parse_content_encoding(self, message: Message, system_properties) -> str: + content_encoding = "" + + if "content_encoding" in system_properties: + content_encoding = system_properties["content_encoding"] + + if not content_encoding: + details = strings.invalid_encoding_none_found() + self._add_issue(severity=Severity.error, details=details) + return None + + if "utf-8" not in content_encoding.lower(): + details = strings.invalid_encoding(content_encoding.lower()) + self._add_issue(severity=Severity.error, details=details) + return None + + return content_encoding + + def _parse_content_type(self, content_type_hint, system_properties) -> str: + content_type = "" + if content_type_hint: + content_type = content_type_hint + elif "content_type" in system_properties: + content_type = system_properties["content_type"] + + if not content_type: + details = strings.invalid_encoding_missing(system_properties) + self._add_issue(severity=Severity.error, details=details) + + return content_type + + def _parse_annotations(self, message: Message): + try: + return unicode_binary_map(message.annotations) + except Exception: + details = strings.invalid_annotations(message) + self._add_issue(severity=Severity.error, details=details) + return {} + + def _parse_application_properties(self, message: Message): + try: + return unicode_binary_map(message.application_properties) + except Exception: + details = strings.invalid_application_properties(message) + self._add_issue(severity=Severity.error, details=details) + return {} + + def _parse_payload(self, message: Message, content_type): + payload = "" + data = message.get_data() + + if data: + payload = str(next(data), "utf8") + + if "application/json" not in content_type.lower(): + details = strings.invalid_content_type(content_type.lower()) + self._add_issue(severity=Severity.warning, details=details) + else: + try: + regex = r"(\\r\\n)+|\\r+|\\n+" + payload_no_white_space = re.compile(regex).sub("", payload) + payload = json.loads(payload_no_white_space) + except Exception: + details = strings.invalid_json() + self._add_issue(severity=Severity.error, details=details) + + return payload diff --git a/azext_iot/monitor/parsers/issue.py b/azext_iot/monitor/parsers/issue.py new file mode 100644 index 000000000..72a29be55 --- /dev/null +++ b/azext_iot/monitor/parsers/issue.py @@ -0,0 +1,130 @@ +# 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.log import get_logger + +from azext_iot.monitor.models.enum import Severity + +logger = get_logger(__name__) + + +class Issue: + def __init__(self, severity: Severity, details: str, message, device_id=""): + self.severity = severity + self.details = details + self.device_id = device_id + self.message = str(message) + + if not self.device_id: + self.device_id = "Unknown" + + def log(self): + to_log = "[{}] [DeviceId: {}] {}\n".format( + self.severity.name.upper(), self.device_id, self.details + ) + + self._log(to_log) + + def _log(self, to_log: str): + if self.severity == Severity.info: + logger.info(to_log) + + if self.severity == Severity.warning: + logger.warn(to_log) + + if self.severity == Severity.error: + logger.error(to_log) + + def json_repr(self): + json_repr = vars(self) + json_repr["severity"] = self.severity.name + return json_repr + + +class CentralIssue(Issue): + def __init__( + self, severity: Severity, details: str, message, device_id="", template_id="" + ): + super(CentralIssue, self).__init__(severity, details, message, device_id) + self.template_id = template_id + + if not self.template_id: + self.template_id = "Unknown" + + def log(self): + to_log = "[{}] [DeviceId: {}] [TemplateId: {}] {}\n".format( + self.severity.name.upper(), self.device_id, self.template_id, self.details + ) + + self._log(to_log) + + +class IssueHandler: + def __init__(self): + self._issues = [] + + def add_issue(self, severity: Severity, details: str, message, device_id=""): + issue = Issue( + severity=severity, details=details, message=message, device_id=device_id + ) + self._issues.append(issue) + + def add_central_issue( + self, severity: Severity, details: str, message, device_id="", template_id="" + ): + issue = CentralIssue( + severity=severity, + details=details, + message=message, + device_id=device_id, + template_id=template_id, + ) + self._issues.append(issue) + + def get_all_issues(self) -> List[Issue]: + return self._issues + + def get_issues_with_severity(self, severity: Severity) -> List[Issue]: + """ + arguments: + severity: Severity + returns: + all issues where severity equal specified severity + + example: + issue_handler.get_issues_with_severity(Severity.info) + returns all issues classified as "info" + """ + return [issue for issue in self._issues if issue.severity == severity] + + def get_issues_with_minimum_severity(self, severity: Severity) -> List[Issue]: + """ + arguments: + severity: Severity + returns: + all issues where severity >= specified severity + + example: + issue_handler.get_issues_with_minimum_severity(Severity.warning) + returns all issues classified as "warning" and "error" + "info" will not be included + """ + return [issue for issue in self._issues if issue.severity >= severity] + + def get_issues_with_maximum_severity(self, severity: Severity) -> List[Issue]: + """ + arguments: + severity: Severity + returns: + all issues where severity <= specified severity + + example: + issue_handler.get_issues_with_maximum_severity(Severity.warning) + returns all issues classified as "warning" and "info" + "error" will not be included + """ + return [issue for issue in self._issues if issue.severity <= severity] diff --git a/azext_iot/monitor/parsers/strings.py b/azext_iot/monitor/parsers/strings.py new file mode 100644 index 000000000..cbd443e65 --- /dev/null +++ b/azext_iot/monitor/parsers/strings.py @@ -0,0 +1,116 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +SUPPORTED_ENCODING = ["utf-8"] +SUPPORTED_FIELD_NAME_CHARS = ["a-z", "A-Z", "0-9", "_"] +SUPPORTED_CONTENT_TYPE = ["application/json"] +SUPPORTED_MESSAGE_HEADERS = [] + + +def unknown_device_id(): + return "Device ID not found in message".format() + + +def invalid_json(): + return "Invalid JSON format.".format() + + +def invalid_encoding(encoding: str): + return "Encoding type '{}' is not supported. Expected encoding '{}'.".format( + encoding, SUPPORTED_ENCODING + ) + + +def invalid_field_name(invalid_field_names: list): + return ( + "Invalid characters detected. Invalid field names: '{}'. " + "Allowed characters: {}." + ).format(invalid_field_names, SUPPORTED_FIELD_NAME_CHARS) + + +def invalid_pnp_property_not_value_wrapped(): + raise NotImplementedError() + + +def invalid_non_pnp_field_name_duplicate(): + raise NotImplementedError() + + +def invalid_content_type(content_type: str): + return "Content type '{}' is not supported. Expected Content type: {}.".format( + content_type, SUPPORTED_CONTENT_TYPE + ) + + +def invalid_custom_headers(): + return ( + "Custom message headers are not supported in IoT Central. " + "All the custom message headers will be dropped. " + "Supported message headers: '{}'." + ).format(SUPPORTED_MESSAGE_HEADERS) + + +def invalid_field_name_mismatch_template( + unmodeled_capabilities: list, modeled_capabilities: list +): + return ( + "Device is sending data that has not been defined in the device template. " + "Following capabilities have NOT been defined in the device template '{}'. " + "Following capabilities have been defined in the device template '{}'. " + ).format(unmodeled_capabilities, modeled_capabilities) + + +def invalid_primitive_schema_mismatch_template(field_name: str, data_type: str, data): + return ( + "Datatype of field '{}' does not match the datatype '{}'. Data '{}'. " + "All dates/times/datetimes/durations must be ISO 8601 compliant.".format( + field_name, data_type, data, + ) + ) + + +def invalid_interface_name_not_found(): + return "Interface name not found." + + +def invalid_interface_name_mismatch( + expected_interface_name: str, actual_interface_name: str +): + return "Inteface name mismatch. Expected: {}, Actual: {}.".format( + expected_interface_name, actual_interface_name + ) + + +def invalid_system_properties(): + return "Failed to parse system properties.".format() + + +def invalid_encoding_none_found(): + return "No encoding found. Expected encoding 'utf-8' to be present in message header.".format() + + +def invalid_encoding_missing(system_properties: dict): + return "Content type not found in system properties. System properties: {}.".format( + system_properties + ) + + +def invalid_annotations(message): + return "Unable to decode message.annotations: {}.".format(message.annotations) + + +def invalid_application_properties(message): + return "Unable to decode message.application_properties: {}.".format( + message.application_properties + ) + + +def device_template_not_found(error: Exception): + return "Error retrieving template '{}'".format(error) + + +def invalid_template_extract_schema_failed(template: dict): + return "Unable to extract device schema from template '{}'".format(template) diff --git a/azext_iot/monitor/telemetry.py b/azext_iot/monitor/telemetry.py index 598da2958..a4e756e1c 100644 --- a/azext_iot/monitor/telemetry.py +++ b/azext_iot/monitor/telemetry.py @@ -13,6 +13,7 @@ from typing import List from azext_iot.constants import VERSION, USER_AGENT from azext_iot.monitor.models.target import Target +from azext_iot.monitor.utility import get_loop logger = get_logger(__name__) DEBUG = False @@ -61,10 +62,7 @@ def start_multiple_monitors( for target in targets ] - loop = asyncio.get_event_loop() - if loop.is_closed(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + loop = get_loop() future = asyncio.gather(*coroutines, loop=loop, return_exceptions=True) result = None diff --git a/azext_iot/monitor/utility.py b/azext_iot/monitor/utility.py index 5155f53cd..ada95186a 100644 --- a/azext_iot/monitor/utility.py +++ b/azext_iot/monitor/utility.py @@ -4,13 +4,28 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import asyncio +from asyncio import AbstractEventLoop -def generate_on_start_string(device_id, pnp_context): + +def generate_on_start_string(device_id=None): device_filter_txt = None if device_id: device_filter_txt = " filtering on device: {},".format(device_id) - return "Starting {}event monitor,{} use ctrl-c to stop...".format( - "Digital Twin " if pnp_context else "", + return "Starting event monitor,{} use ctrl-c to stop...".format( device_filter_txt if device_filter_txt else "", ) + + +def stop_monitor(): + raise KeyboardInterrupt() + + +def get_loop() -> AbstractEventLoop: + loop = asyncio.get_event_loop() + if loop.is_closed(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + return loop diff --git a/azext_iot/operations/central.py b/azext_iot/operations/central.py deleted file mode 100644 index 4c234464a..000000000 --- a/azext_iot/operations/central.py +++ /dev/null @@ -1,143 +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 knack.util import CLIError -from azext_iot._factory import _bind_sdk -from azext_iot.common.shared import SdkType -from azext_iot.common.utility import unpack_msrest_error, init_monitoring -from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication -from azext_iot.central.providers import CentralDeviceProvider - - -def find_between(s, start, end): - return (s.split(start))[1].split(end)[0] - - -def iot_central_device_show( - cmd, device_id, app_id, central_api_uri="azureiotcentral.com" -): - from azext_iot.common._azure import get_iot_central_tokens - - tokens = get_iot_central_tokens(cmd, app_id, central_api_uri) - exception = None - - # The device could be in any hub associated with the given app. - # We must search through each IoT Hub until device is found. - for token_group in tokens.values(): - sas_token = token_group["iothubTenantSasToken"]["sasToken"] - endpoint = find_between(sas_token, "SharedAccessSignature sr=", "&sig=") - target = {"entity": endpoint} - auth = BasicSasTokenAuthentication(sas_token=sas_token) - service_sdk, errors = _bind_sdk(target, SdkType.service_sdk, auth=auth) - try: - return service_sdk.get_twin(device_id) - except errors.CloudError as e: - if exception is None: - exception = CLIError(unpack_msrest_error(e)) - - raise exception - - -def iot_central_validate_messages( - cmd, - app_id, - device_id=None, - simulate_errors=False, - consumer_group="$Default", - timeout=300, - enqueued_time=None, - repair=False, - properties=None, - yes=False, - central_api_uri="azureiotcentral.com", -): - provider = CentralDeviceProvider(cmd, app_id) - _events3_runner( - cmd=cmd, - app_id=app_id, - device_id=device_id, - validate_messages=True, - simulate_errors=simulate_errors, - consumer_group=consumer_group, - timeout=timeout, - enqueued_time=enqueued_time, - repair=repair, - properties=properties, - yes=yes, - central_api_uri=central_api_uri, - central_device_provider=provider, - ) - - -def iot_central_monitor_events( - cmd, - app_id, - device_id=None, - consumer_group="$Default", - timeout=300, - enqueued_time=None, - repair=False, - properties=None, - yes=False, - central_api_uri="azureiotcentral.com", -): - _events3_runner( - cmd=cmd, - app_id=app_id, - device_id=device_id, - validate_messages=False, - simulate_errors=False, - consumer_group=consumer_group, - timeout=timeout, - enqueued_time=enqueued_time, - repair=repair, - properties=properties, - yes=yes, - central_api_uri=central_api_uri, - central_device_provider=None, - ) - - -def _events3_runner( - cmd, - app_id, - device_id, - validate_messages, - simulate_errors, - consumer_group, - timeout, - enqueued_time, - repair, - properties, - yes, - central_api_uri, - central_device_provider, -): - (enqueued_time, properties, timeout, output) = init_monitoring( - cmd, timeout, properties, enqueued_time, repair, yes - ) - - from azext_iot.monitor.builders import central_target_builder - from azext_iot.monitor.handlers import CommonHandler - from azext_iot.monitor.utility import generate_on_start_string - from azext_iot.monitor import telemetry - - on_start_string = generate_on_start_string(device_id=device_id, pnp_context=None) - - targets = central_target_builder.build_central_event_hub_targets( - cmd, app_id, central_api_uri - ) - [target.add_consumer_group(consumer_group) for target in targets] - - handler = CommonHandler(device_id=device_id, properties=properties, output=output) - - telemetry.start_multiple_monitors( - targets=targets, - enqueued_time_utc=enqueued_time, - on_start_string=on_start_string, - on_message_received=handler.parse_message, - timeout=timeout, - ) diff --git a/azext_iot/operations/digitaltwin.py b/azext_iot/operations/digitaltwin.py index 2946cb103..249ca1b58 100644 --- a/azext_iot/operations/digitaltwin.py +++ b/azext_iot/operations/digitaltwin.py @@ -10,41 +10,62 @@ from azext_iot._factory import _bind_sdk from azext_iot.common.shared import SdkType, ModelSourceType from azext_iot.common._azure import get_iot_hub_connection_string -from azext_iot.common.utility import (shell_safe_json_parse, - read_file_content, - unpack_msrest_error) -from azext_iot.operations.pnp import (iot_pnp_interface_show, - iot_pnp_interface_list, - _validate_repository) +from azext_iot.common.utility import ( + shell_safe_json_parse, + read_file_content, + unpack_msrest_error, +) +from azext_iot.operations.pnp import ( + iot_pnp_interface_show, + iot_pnp_interface_list, + _validate_repository, +) from azext_iot.operations.hub import _iot_hub_monitor_events -INTERFACE_KEY_NAME = 'urn_azureiot_ModelDiscovery_DigitalTwin' -INTERFACE_COMMAND = 'Command' -INTERFACE_PROPERTY = 'Property' -INTERFACE_TELEMETRY = 'Telemetry' -INTERFACE_MODELDEFINITION = 'urn_azureiot_ModelDiscovery_ModelDefinition' -INTERFACE_COMMANDNAME = 'getModelDefinition' +INTERFACE_KEY_NAME = "urn_azureiot_ModelDiscovery_DigitalTwin" +INTERFACE_COMMAND = "Command" +INTERFACE_PROPERTY = "Property" +INTERFACE_TELEMETRY = "Telemetry" +INTERFACE_MODELDEFINITION = "urn_azureiot_ModelDiscovery_ModelDefinition" +INTERFACE_COMMANDNAME = "getModelDefinition" -def iot_digitaltwin_interface_list(cmd, device_id, hub_name=None, resource_group_name=None, login=None): - device_default_interface = _iot_digitaltwin_interface_show(cmd, device_id, INTERFACE_KEY_NAME, - hub_name, resource_group_name, login) +def iot_digitaltwin_interface_list( + cmd, device_id, hub_name=None, resource_group_name=None, login=None +): + device_default_interface = _iot_digitaltwin_interface_show( + cmd, device_id, INTERFACE_KEY_NAME, hub_name, resource_group_name, login + ) result = _get_device_default_interface_dict(device_default_interface) - return {'interfaces': result} - - -def iot_digitaltwin_command_list(cmd, device_id, source_model, interface=None, schema=False, - repo_endpoint=PNP_ENDPOINT, repo_id=None, repo_login=None, - hub_name=None, resource_group_name=None, login=None): + return {"interfaces": result} + + +def iot_digitaltwin_command_list( + cmd, + device_id, + source_model, + interface=None, + schema=False, + repo_endpoint=PNP_ENDPOINT, + repo_id=None, + repo_login=None, + hub_name=None, + resource_group_name=None, + login=None, +): result = [] target_interfaces = [] source_model = source_model.lower() - device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login) + device_interfaces = _iot_digitaltwin_interface_list( + cmd, device_id, hub_name, resource_group_name, login + ) interface_list = _get_device_default_interface_dict(device_interfaces) - target_interface = next((item for item in interface_list if item['name'] == interface), None) + target_interface = next( + (item for item in interface_list if item["name"] == interface), None + ) if interface and not target_interface: - raise CLIError('Target interface is not implemented by the device!') + raise CLIError("Target interface is not implemented by the device!") if interface: target_interfaces.append(target_interface) @@ -52,39 +73,70 @@ def iot_digitaltwin_command_list(cmd, device_id, source_model, interface=None, s target_interfaces = interface_list for entity in target_interfaces: - interface_result = {'name': entity['name'], 'urn_id': entity['urn_id'], 'commands': {}} + interface_result = { + "name": entity["name"], + "urn_id": entity["urn_id"], + "commands": {}, + } interface_commands = [] found_commands = [] if source_model == ModelSourceType.device.value.lower(): - found_commands = _device_interface_elements(cmd, device_id, entity['urn_id'], INTERFACE_COMMAND, - hub_name, resource_group_name, login) + found_commands = _device_interface_elements( + cmd, + device_id, + entity["urn_id"], + INTERFACE_COMMAND, + hub_name, + resource_group_name, + login, + ) else: if source_model == ModelSourceType.private.value.lower(): _validate_repository(repo_id, repo_login) - found_commands = _pnp_interface_elements(cmd, entity['urn_id'], INTERFACE_COMMAND, - repo_endpoint, repo_id, repo_login) + found_commands = _pnp_interface_elements( + cmd, + entity["urn_id"], + INTERFACE_COMMAND, + repo_endpoint, + repo_id, + repo_login, + ) for command in found_commands: - command.pop('@type', None) + command.pop("@type", None) if schema: interface_commands.append(command) else: - interface_commands.append(command.get('name')) - interface_result['commands'] = interface_commands + interface_commands.append(command.get("name")) + interface_result["commands"] = interface_commands result.append(interface_result) - return {'interfaces': result} - - -def iot_digitaltwin_properties_list(cmd, device_id, source_model, interface=None, schema=False, - repo_endpoint=PNP_ENDPOINT, repo_id=None, repo_login=None, - hub_name=None, resource_group_name=None, login=None): + return {"interfaces": result} + + +def iot_digitaltwin_properties_list( + cmd, + device_id, + source_model, + interface=None, + schema=False, + repo_endpoint=PNP_ENDPOINT, + repo_id=None, + repo_login=None, + hub_name=None, + resource_group_name=None, + login=None, +): result = [] target_interfaces = [] source_model = source_model.lower() - device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login) + device_interfaces = _iot_digitaltwin_interface_list( + cmd, device_id, hub_name, resource_group_name, login + ) interface_list = _get_device_default_interface_dict(device_interfaces) - target_interface = next((item for item in interface_list if item['name'] == interface), None) + target_interface = next( + (item for item in interface_list if item["name"] == interface), None + ) if interface and not target_interface: - raise CLIError('Target interface is not implemented by the device!') + raise CLIError("Target interface is not implemented by the device!") if interface: target_interfaces.append(target_interface) @@ -92,37 +144,67 @@ def iot_digitaltwin_properties_list(cmd, device_id, source_model, interface=None target_interfaces = interface_list for entity in target_interfaces: - interface_result = {'name': entity['name'], 'urn_id': entity['urn_id'], 'properties': {}} + interface_result = { + "name": entity["name"], + "urn_id": entity["urn_id"], + "properties": {}, + } interface_properties = [] found_properties = [] if source_model == ModelSourceType.device.value.lower(): - found_properties = _device_interface_elements(cmd, device_id, entity['urn_id'], INTERFACE_PROPERTY, - hub_name, resource_group_name, login) + found_properties = _device_interface_elements( + cmd, + device_id, + entity["urn_id"], + INTERFACE_PROPERTY, + hub_name, + resource_group_name, + login, + ) else: if source_model == ModelSourceType.private.value.lower(): _validate_repository(repo_id, repo_login) - found_properties = _pnp_interface_elements(cmd, entity['urn_id'], INTERFACE_PROPERTY, - repo_endpoint, repo_id, repo_login) + found_properties = _pnp_interface_elements( + cmd, + entity["urn_id"], + INTERFACE_PROPERTY, + repo_endpoint, + repo_id, + repo_login, + ) for prop in found_properties: - prop.pop('@type', None) + prop.pop("@type", None) if schema: interface_properties.append(prop) else: - interface_properties.append(prop.get('name')) - interface_result['properties'] = interface_properties + interface_properties.append(prop.get("name")) + interface_result["properties"] = interface_properties result.append(interface_result) - return {'interfaces': result} - - -def iot_digitaltwin_invoke_command(cmd, interface, device_id, command_name, command_payload=None, - timeout=10, hub_name=None, resource_group_name=None, login=None): - device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login) + return {"interfaces": result} + + +def iot_digitaltwin_invoke_command( + cmd, + interface, + device_id, + command_name, + command_payload=None, + timeout=10, + hub_name=None, + resource_group_name=None, + login=None, +): + device_interfaces = _iot_digitaltwin_interface_list( + cmd, device_id, hub_name, resource_group_name, login + ) interface_list = _get_device_default_interface_dict(device_interfaces) - target_interface = next((item for item in interface_list if item['name'] == interface), None) + target_interface = next( + (item for item in interface_list if item["name"] == interface), None + ) if not target_interface: - raise CLIError('Target interface is not implemented by the device!') + raise CLIError("Target interface is not implemented by the device!") if command_payload: if exists(command_payload): @@ -137,22 +219,32 @@ def iot_digitaltwin_invoke_command(cmd, interface, device_id, command_name, comm if target_json or isinstance(target_json, bool): command_payload = target_json - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) try: - result = service_sdk.invoke_interface_command(device_id, - interface, - command_name, - command_payload, - connect_timeout_in_seconds=timeout, - response_timeout_in_seconds=timeout) + result = service_sdk.invoke_interface_command( + device_id, + interface, + command_name, + command_payload, + connect_timeout_in_seconds=timeout, + response_timeout_in_seconds=timeout, + ) return result except errors.CloudError as e: raise CLIError(unpack_msrest_error(e)) -def iot_digitaltwin_property_update(cmd, interface_payload, device_id, - hub_name=None, resource_group_name=None, login=None): +def iot_digitaltwin_property_update( + cmd, + interface_payload, + device_id, + hub_name=None, + resource_group_name=None, + login=None, +): if exists(interface_payload): interface_payload = str(read_file_content(interface_payload)) @@ -165,7 +257,9 @@ def iot_digitaltwin_property_update(cmd, interface_payload, device_id, if target_json: interface_payload = target_json - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) try: result = service_sdk.update_interfaces(device_id, interfaces=interface_payload) @@ -174,18 +268,46 @@ def iot_digitaltwin_property_update(cmd, interface_payload, device_id, raise CLIError(unpack_msrest_error(e)) -def iot_digitaltwin_monitor_events(cmd, hub_name=None, device_id=None, consumer_group='$Default', timeout=300, - enqueued_time=None, resource_group_name=None, yes=False, properties=None, repair=False, - login=None, content_type=None, device_query=None, interface=None): - _iot_hub_monitor_events(cmd=cmd, hub_name=hub_name, device_id=device_id, - consumer_group=consumer_group, timeout=timeout, enqueued_time=enqueued_time, - resource_group_name=resource_group_name, yes=yes, properties=properties, - repair=repair, login=login, content_type=content_type, device_query=device_query, - interface_name=interface, pnp_context=True) - - -def _iot_digitaltwin_interface_show(cmd, device_id, interface, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def iot_digitaltwin_monitor_events( + cmd, + hub_name=None, + device_id=None, + consumer_group="$Default", + timeout=300, + enqueued_time=None, + resource_group_name=None, + yes=False, + properties=None, + repair=False, + login=None, + content_type=None, + device_query=None, + interface=None, +): + _iot_hub_monitor_events( + cmd=cmd, + hub_name=hub_name, + device_id=device_id, + consumer_group=consumer_group, + timeout=timeout, + enqueued_time=enqueued_time, + resource_group_name=resource_group_name, + yes=yes, + properties=properties, + repair=repair, + login=login, + content_type=content_type, + device_query=device_query, + interface_name=interface, + ) + + +def _iot_digitaltwin_interface_show( + cmd, device_id, interface, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) try: device_interface = service_sdk.get_interface(device_id, interface) @@ -194,8 +316,12 @@ def _iot_digitaltwin_interface_show(cmd, device_id, interface, hub_name=None, re raise CLIError(unpack_msrest_error(e)) -def _iot_digitaltwin_interface_list(cmd, device_id, hub_name=None, resource_group_name=None, login=None): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def _iot_digitaltwin_interface_list( + cmd, device_id, hub_name=None, resource_group_name=None, login=None +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) try: device_interfaces = service_sdk.get_interfaces(device_id) @@ -205,45 +331,58 @@ def _iot_digitaltwin_interface_list(cmd, device_id, hub_name=None, resource_grou def _get_device_default_interface_dict(device_default_interface): - interface = device_default_interface['interfaces'][INTERFACE_KEY_NAME] + interface = device_default_interface["interfaces"][INTERFACE_KEY_NAME] result = [] - for k, v in interface['properties']['modelInformation']['reported']['value']['interfaces'].items(): - result.append({'name': k, "urn_id": v}) + for k, v in interface["properties"]["modelInformation"]["reported"]["value"][ + "interfaces" + ].items(): + result.append({"name": k, "urn_id": v}) return result def _pnp_interface_elements(cmd, interface, target_type, repo_endpoint, repo_id, login): interface_elements = [] - results = iot_pnp_interface_list(cmd, repo_endpoint, repo_id, interface, login=login) + results = iot_pnp_interface_list( + cmd, repo_endpoint, repo_id, interface, login=login + ) if results: - interface_def = iot_pnp_interface_show(cmd, interface, repo_endpoint, repo_id, login) - interface_contents = interface_def.get('contents') + interface_def = iot_pnp_interface_show( + cmd, interface, repo_endpoint, repo_id, login + ) + interface_contents = interface_def.get("contents") for content in interface_contents: - if isinstance(content.get('@type'), list) and target_type in content.get('@type'): + if isinstance(content.get("@type"), list) and target_type in content.get( + "@type" + ): interface_elements.append(content) - elif content.get('@type') == target_type: + elif content.get("@type") == target_type: interface_elements.append(content) return interface_elements -def _device_interface_elements(cmd, device_id, interface, target_type, hub_name, resource_group_name, login): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login) +def _device_interface_elements( + cmd, device_id, interface, target_type, hub_name, resource_group_name, login +): + target = get_iot_hub_connection_string( + cmd, hub_name, resource_group_name, login=login + ) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) interface_elements = [] try: - payload = {'id': {}} - payload['id'] = interface + payload = {"id": {}} + payload["id"] = interface target_payload = shell_safe_json_parse(str(payload)) - interface_def = service_sdk.invoke_interface_command(device_id, - INTERFACE_MODELDEFINITION, - INTERFACE_COMMANDNAME, - target_payload) - if interface_def and interface_def.get('contents'): - interface_contents = interface_def.get('contents') + interface_def = service_sdk.invoke_interface_command( + device_id, INTERFACE_MODELDEFINITION, INTERFACE_COMMANDNAME, target_payload + ) + if interface_def and interface_def.get("contents"): + interface_contents = interface_def.get("contents") for content in interface_contents: - if isinstance(content.get('@type'), list) and target_type in content.get('@type'): + if isinstance( + content.get("@type"), list + ) and target_type in content.get("@type"): interface_elements.append(content) - elif content.get('@type') == target_type: + elif content.get("@type") == target_type: interface_elements.append(content) return interface_elements except errors.CloudError as e: diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index ba5272e1e..35499b7ae 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -2201,7 +2201,6 @@ def iot_hub_distributed_tracing_show( def _iot_hub_monitor_events( cmd, interface_name=None, - pnp_context=None, hub_name=None, device_id=None, consumer_group="$Default", @@ -2236,24 +2235,28 @@ def _iot_hub_monitor_events( from azext_iot.monitor.handlers import CommonHandler from azext_iot.monitor.telemetry import start_single_monitor from azext_iot.monitor.utility import generate_on_start_string + from azext_iot.monitor.models.arguments import ( + CommonParserArguments, + CommonHandlerArguments, + ) target = hub_target_builder.EventTargetBuilder().build_iot_hub_target(target) target.add_consumer_group(consumer_group) - on_start_string = generate_on_start_string( - device_id=device_id, pnp_context=pnp_context - ) + on_start_string = generate_on_start_string(device_id=device_id) - handler = CommonHandler( - device_id=device_id, - devices=device_ids, - pnp_context=pnp_context, - interface_name=interface_name, - content_type=content_type, - properties=properties, + parser_args = CommonParserArguments( + properties=properties, interface_name=interface_name, content_type=content_type + ) + handler_args = CommonHandlerArguments( output=output, + common_parser_args=parser_args, + devices=device_ids, + device_id=device_id, ) + handler = CommonHandler(handler_args) + start_single_monitor( target=target, enqueued_time_utc=enqueued_time, diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 4b3dc291f..fd9108069 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -65,7 +65,7 @@ def test_central_device_twin_show_success(self): (device_id, _) = self._create_device(instance_of=template_id, simulated=True) # wait about a few seconds for simulator to kick in so that provisioning completes - time.sleep(10) + time.sleep(60) self.cmd( "iotcentral device-twin show --app-id {} --device-id {}".format( diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index f35c00f9b..248eda325 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -9,7 +9,8 @@ from knack.util import CLIError from azure.cli.core.mock import DummyCli -from azext_iot.operations import central as subject +from azext_iot.central import commands_device_twin +from azext_iot.central import commands_monitor from azext_iot.common.shared import SdkType from azext_iot.central.providers import ( CentralDeviceProvider, @@ -40,7 +41,7 @@ class mock_service_sdk: def get_twin(self, device_id): return device_twin_result - mock = mocker.patch("azext_iot.operations.central._bind_sdk") + mock = mocker.patch("azext_iot.central.commands_device_twin._bind_sdk") mock.return_value = (mock_service_sdk(), None) return mock @@ -132,7 +133,7 @@ class TestDeviceTwinShow: def test_device_twin_show_calls_get_twin( self, fixture_bind_sdk, fixture_cmd, fixture_get_iot_central_tokens ): - result = subject.iot_central_device_show(fixture_cmd, device_id, app_id) + result = commands_device_twin.device_twin_show(fixture_cmd, device_id, app_id) # Ensure get_twin is called and result is returned assert result is device_twin_result @@ -147,7 +148,7 @@ class TestMonitorEvents: @pytest.mark.parametrize("timeout, exception", [(-1, CLIError)]) def test_monitor_events_invalid_args(self, timeout, exception, fixture_cmd): with pytest.raises(exception): - subject.iot_central_monitor_events(fixture_cmd, app_id, timeout=timeout) + commands_monitor.monitor_events(fixture_cmd, app_id, timeout=timeout) class TestCentralDeviceProvider: diff --git a/azext_iot/tests/test_iot_digitaltwin_unit.py b/azext_iot/tests/test_iot_digitaltwin_unit.py index ae1320ba5..25aca9d62 100644 --- a/azext_iot/tests/test_iot_digitaltwin_unit.py +++ b/azext_iot/tests/test_iot_digitaltwin_unit.py @@ -587,8 +587,6 @@ def test_iot_digitaltwin_monitor_events_entrypoint( monitor_events_args = fixture_monitor_events_entrypoint.call_args[1] - assert monitor_events_args["pnp_context"] - dt_attribute_set = [ "device_id", "device_query", diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index 2737da5f9..1bc9fe037 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -1984,7 +1984,6 @@ def test_monitor_events_entrypoint( "repair", ] - assert "pnp_context" not in monitor_events_args assert "interface" not in monitor_events_args for attribute in attribute_set: diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index df69f8a52..194e73572 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -9,9 +9,7 @@ import pytest from knack.util import CLIError -from uamqp.message import Message, MessageProperties from azure.cli.core.extension import get_extension_path -from azext_iot.central.providers import CentralDeviceProvider from azext_iot.common.utility import ( validate_min_python_version, process_json_arg, @@ -21,10 +19,7 @@ ) from azext_iot.common.deps import ensure_uamqp from azext_iot.constants import EVENT_LIB, EXTENSION_NAME -from azext_iot.monitor.handlers import _parser from azext_iot._validators import mode2_iot_login_handler -from .helpers import load_json -from .test_constants import FileNames class TestMinPython(object): @@ -350,434 +345,3 @@ def test_is_iso8601_duration_fail(self, to_validate): def test_is_iso8601_time_fail(self, to_validate): result = self.validator.is_iso8601_time(to_validate) assert not result - - -class TestEvents3Parser: - device_id = "some-device-id" - payload = {"String": "someValue"} - encoding = "UTF-8" - content_type = "application/json" - - bad_encoding = "ascii" - bad_payload = "bad-payload" - bad_field_name = {"violates-regex": "someValue"} - bad_content_type = "bad-content-type" - - bad_dcm_payload = {"temperature": "someValue"} - type_mismatch_payload = {"Bool": "someValue"} - - def test_parse_message_should_succeed(self): - # setup - app_prop_type = "some_property" - app_prop_value = "some_value" - properties = MessageProperties( - content_encoding=self.encoding, content_type=self.content_type - ) - message = Message( - body=json.dumps(self.payload).encode(), - properties=properties, - annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, - application_properties={app_prop_type.encode(): app_prop_value.encode()}, - ) - parser = _parser.Event3Parser() - - device_template = load_json(FileNames.central_device_template_file) - provider = CentralDeviceProvider(cmd=None, app_id=None) - provider.get_device_template_by_device_id = mock.MagicMock( - return_value=device_template - ) - - # act - parsed_msg = parser.parse_message( - message=message, - pnp_context=False, - interface_name=None, - properties={"all"}, - content_type_hint=None, - simulate_errors=False, - central_device_provider=provider, - ) - - # verify - assert parsed_msg["event"]["payload"] == self.payload - assert parsed_msg["event"]["origin"] == self.device_id - device_identifier = str(_parser.DEVICE_ID_IDENTIFIER, "utf8") - assert parsed_msg["event"]["annotations"][device_identifier] == self.device_id - - properties = parsed_msg["event"]["properties"] - assert properties["system"]["content_encoding"] == self.encoding - assert properties["system"]["content_type"] == self.content_type - assert properties["application"][app_prop_type] == app_prop_value - - assert len(parser._errors) == 0 - assert len(parser._warnings) == 0 - assert len(parser._info) == 0 - - def test_parse_message_pnp_should_succeed(self): - # setup - interface_name = "interface_name" - properties = MessageProperties( - content_encoding=self.encoding, content_type=self.content_type - ) - message = Message( - body=json.dumps(self.payload).encode(), - properties=properties, - annotations={ - _parser.DEVICE_ID_IDENTIFIER: self.device_id.encode(), - _parser.INTERFACE_NAME_IDENTIFIER: interface_name.encode(), - }, - ) - parser = _parser.Event3Parser() - - # act - parsed_msg = parser.parse_message( - message=message, - pnp_context=True, - interface_name=interface_name, - properties=None, - content_type_hint=None, - simulate_errors=False, - central_device_provider=None, - ) - - # verify - assert parsed_msg["event"]["payload"] == self.payload - assert parsed_msg["event"]["origin"] == self.device_id - assert parsed_msg["event"]["interface"] == interface_name - - assert len(parser._errors) == 0 - assert len(parser._warnings) == 0 - assert len(parser._info) == 0 - - def test_parse_message_bad_content_type_should_warn(self): - # setup - encoded_payload = json.dumps(self.payload).encode() - properties = MessageProperties(content_type=self.bad_content_type) - message = Message( - body=encoded_payload, - properties=properties, - annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, - ) - parser = _parser.Event3Parser() - - # act - parsed_msg = parser.parse_message( - message=message, - pnp_context=False, - interface_name=None, - properties=None, - content_type_hint=None, - simulate_errors=False, - central_device_provider=None, - ) - - # verify - # since the content_encoding header is not present, just dump the raw payload - assert parsed_msg["event"]["payload"] == str(encoded_payload, "utf8") - - assert len(parser._errors) == 1 - assert len(parser._warnings) == 1 - assert len(parser._info) == 0 - - warning = parser._warnings[0] - assert "Content type not supported." in warning - assert self.bad_content_type in warning - assert "application/json" in warning - assert self.device_id in warning - - error = parser._errors[0] - assert "No encoding found for message" in error - - def test_parse_message_bad_encoding_should_fail(self): - # setup - properties = MessageProperties( - content_encoding=self.bad_encoding, content_type=self.content_type - ) - message = Message( - body=json.dumps(self.payload).encode(self.bad_encoding), - properties=properties, - annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, - ) - parser = _parser.Event3Parser() - - # act - parser.parse_message( - message=message, - pnp_context=False, - interface_name=None, - properties=None, - content_type_hint=None, - simulate_errors=False, - central_device_provider=None, - ) - - assert len(parser._errors) == 1 - assert len(parser._warnings) == 0 - assert len(parser._info) == 0 - - errors = parser._errors[0] - assert "Unsupported encoding detected: '{}'".format(self.bad_encoding) in errors - - def test_parse_message_bad_json_should_fail(self): - # setup - properties = MessageProperties( - content_encoding=self.encoding, content_type=self.content_type - ) - message = Message( - body=self.bad_payload.encode(), - properties=properties, - annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, - ) - parser = _parser.Event3Parser() - - # act - parsed_msg = parser.parse_message( - message=message, - pnp_context=False, - interface_name=None, - properties=None, - content_type_hint=None, - simulate_errors=False, - central_device_provider=None, - ) - - # verify - # parsing should attempt to place raw payload into result even if parsing fails - assert parsed_msg["event"]["payload"] == self.bad_payload - - assert len(parser._errors) == 1 - assert len(parser._warnings) == 0 - assert len(parser._info) == 0 - - errors = parser._errors[0] - assert "Invalid JSON format." in errors - assert self.device_id in errors - assert self.bad_payload in errors - - def test_parse_message_bad_field_name_should_fail(self): - # setup - properties = MessageProperties( - content_encoding=self.encoding, content_type=self.content_type - ) - message = Message( - body=json.dumps(self.bad_field_name).encode(), - properties=properties, - annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, - ) - parser = _parser.Event3Parser() - - # act - parsed_msg = parser.parse_message( - message=message, - pnp_context=False, - interface_name=None, - properties=None, - content_type_hint=None, - simulate_errors=False, - central_device_provider=None, - ) - - # verify - # parsing should attempt to place raw payload into result even if parsing fails - assert parsed_msg["event"]["payload"] == self.bad_field_name - - assert len(parser._errors) == 1 - assert len(parser._warnings) == 0 - assert len(parser._info) == 0 - - errors = parser._errors[0] - assert "The following field names are not allowed" in errors - assert "{}".format(next(iter(self.bad_field_name))) in errors - assert str(self.bad_field_name) in errors - assert self.device_id in errors - - def test_parse_message_pnp_should_fail(self): - # setup - actual_interface_name = "actual_interface_name" - expected_interface_name = "expected_interface_name" - properties = MessageProperties( - content_encoding=self.encoding, content_type=self.content_type - ) - message = Message( - body=json.dumps(self.payload).encode(), - properties=properties, - annotations={ - _parser.DEVICE_ID_IDENTIFIER: self.device_id.encode(), - _parser.INTERFACE_NAME_IDENTIFIER: actual_interface_name.encode(), - }, - ) - parser = _parser.Event3Parser() - - # act - parsed_msg = parser.parse_message( - message=message, - pnp_context=True, - interface_name=expected_interface_name, - properties=None, - content_type_hint=None, - simulate_errors=False, - central_device_provider=None, - ) - - # verify - # all the items should still be parsed and available, but we should have an error - assert parsed_msg["event"]["payload"] == self.payload - assert parsed_msg["event"]["origin"] == self.device_id - assert parsed_msg["event"]["interface"] == actual_interface_name - - assert len(parser._errors) == 1 - assert len(parser._warnings) == 0 - assert len(parser._info) == 0 - - actual_error = parser._errors[0] - expected_error = "Inteface name mismatch. {}. Expected: {}, Actual: {}".format( - self.device_id, expected_interface_name, actual_interface_name - ) - assert actual_error == expected_error - - def test_validate_against_template_should_fail(self): - # setup - app_prop_type = "some_property" - app_prop_value = "some_value" - properties = MessageProperties( - content_encoding=self.encoding, content_type=self.content_type - ) - message = Message( - body=json.dumps(self.bad_dcm_payload).encode(), - properties=properties, - annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, - application_properties={app_prop_type.encode(): app_prop_value.encode()}, - ) - parser = _parser.Event3Parser() - - device_template = load_json(FileNames.central_device_template_file) - provider = CentralDeviceProvider(cmd=None, app_id=None) - provider.get_device_template_by_device_id = mock.MagicMock( - return_value=device_template - ) - - # act - parsed_msg = parser.parse_message( - message=message, - pnp_context=False, - interface_name=None, - properties={"all"}, - content_type_hint=None, - simulate_errors=False, - central_device_provider=provider, - ) - - # verify - assert parsed_msg["event"]["payload"] == self.bad_dcm_payload - assert parsed_msg["event"]["origin"] == self.device_id - device_identifier = str(_parser.DEVICE_ID_IDENTIFIER, "utf8") - assert parsed_msg["event"]["annotations"][device_identifier] == self.device_id - - properties = parsed_msg["event"]["properties"] - assert properties["system"]["content_encoding"] == self.encoding - assert properties["system"]["content_type"] == self.content_type - assert properties["application"][app_prop_type] == app_prop_value - - assert len(parser._errors) == 1 - assert len(parser._warnings) == 0 - assert len(parser._info) == 0 - - actual_error = parser._errors[0] - expected_error = "Telemetry item '{}' is not present in capability model.".format( - list(self.bad_dcm_payload)[0] - ) - assert expected_error in actual_error - - def test_validate_against_bad_template_should_not_throw(self): - # setup - app_prop_type = "some_property" - app_prop_value = "some_value" - properties = MessageProperties( - content_encoding=self.encoding, content_type=self.content_type - ) - message = Message( - body=json.dumps(self.bad_dcm_payload).encode(), - properties=properties, - annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, - application_properties={app_prop_type.encode(): app_prop_value.encode()}, - ) - parser = _parser.Event3Parser() - - provider = CentralDeviceProvider(cmd=None, app_id=None) - provider.get_device_template_by_device_id = mock.MagicMock( - return_value="an_unparseable_template" - ) - - # act - parsed_msg = parser.parse_message( - message=message, - pnp_context=False, - interface_name=None, - properties={"all"}, - content_type_hint=None, - simulate_errors=False, - central_device_provider=provider, - ) - - # verify - assert parsed_msg["event"]["payload"] == self.bad_dcm_payload - assert parsed_msg["event"]["origin"] == self.device_id - - assert len(parser._errors) == 1 - assert len(parser._warnings) == 0 - assert len(parser._info) == 0 - - actual_error = parser._errors[0] - assert "Unable to extract device schema for device" in actual_error - - def test_type_mismatch_should_error(self): - # setup - app_prop_type = "some_property" - app_prop_value = "some_value" - properties = MessageProperties( - content_encoding=self.encoding, content_type=self.content_type - ) - message = Message( - body=json.dumps(self.type_mismatch_payload).encode(), - properties=properties, - annotations={_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, - application_properties={app_prop_type.encode(): app_prop_value.encode()}, - ) - parser = _parser.Event3Parser() - - provider = CentralDeviceProvider(cmd=None, app_id=None) - device_template = load_json(FileNames.central_device_template_file) - provider.get_device_template_by_device_id = mock.MagicMock( - return_value=device_template - ) - - # act - parsed_msg = parser.parse_message( - message=message, - pnp_context=False, - interface_name=None, - properties={"all"}, - content_type_hint=None, - simulate_errors=False, - central_device_provider=provider, - ) - - # verify - assert parsed_msg["event"]["payload"] == self.type_mismatch_payload - assert parsed_msg["event"]["origin"] == self.device_id - - assert len(parser._errors) == 1 - assert len(parser._warnings) == 0 - assert len(parser._info) == 0 - - actual_error = parser._errors[0] - assert "Type mismatch" in actual_error - assert "Type mismatch" in actual_error - assert "Value received" in actual_error - assert "Device ID" in actual_error - assert ( - "All dates/times/datetimes/durations must be ISO 8601 compliant." - in actual_error - ) - assert list(self.type_mismatch_payload.values())[0] in actual_error - assert list(self.type_mismatch_payload.keys())[0] in actual_error diff --git a/azext_iot/tests/test_monitor_parsers_unit.py b/azext_iot/tests/test_monitor_parsers_unit.py new file mode 100644 index 000000000..e2c654824 --- /dev/null +++ b/azext_iot/tests/test_monitor_parsers_unit.py @@ -0,0 +1,407 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +import mock +import pytest + +from uamqp.message import Message, MessageProperties +from azext_iot.central.providers import CentralDeviceProvider +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 +from azext_iot.monitor.models.enum import Severity +from .helpers import load_json +from .test_constants import FileNames + + +def _encode_app_props(app_props: dict): + return {key.encode(): value.encode() for key, value in app_props.items()} + + +def _validate_issues( + parser: common_parser.CommonParser, + severity: Severity, + expected_total_issues: int, + expected_specified_issues: int, + expected_detailss: list, +): + issues = parser.issues_handler.get_all_issues() + specified_issues = parser.issues_handler.get_issues_with_severity(severity) + assert len(issues) == expected_total_issues + assert len(specified_issues) == expected_specified_issues + + actual_messages = [issue.details for issue in specified_issues] + for expected_details in expected_detailss: + assert expected_details in actual_messages + + +class TestCommonParser: + device_id = "some-device-id" + payload = {"String": "someValue"} + encoding = "UTF-8" + content_type = "application/json" + + bad_encoding = "ascii" + bad_payload = "bad-payload" + bad_content_type = "bad-content-type" + + @pytest.mark.parametrize( + "device_id, encoding, content_type, interface_name, payload, properties, app_properties", + [ + ( + "device-id", + "utf-8", + "application/json", + "interface_name", + {"payloadKey": "payloadValue"}, + {"propertiesKey": "propertiesValue"}, + {"appPropsKey": "appPropsValue"}, + ), + ( + "device-id", + "utf-8", + "application/json", + "", + {"payloadKey": "payloadValue"}, + {"propertiesKey": "propertiesValue"}, + {"appPropsKey": "appPropsValue"}, + ), + ( + "device-id", + "utf-8", + "application/json", + "", + {}, + {"propertiesKey": "propertiesValue"}, + {"appPropsKey": "appPropsValue"}, + ), + ( + "device-id", + "utf-8", + "application/json", + "", + {}, + {}, + {"appPropsKey": "appPropsValue"}, + ), + ("device-id", "utf-8", "application/json", "", {}, {}, {},), + ], + ) + def test_parse_message_should_succeed( + self, + device_id, + encoding, + content_type, + interface_name, + payload, + properties, + app_properties, + ): + # setup + properties = MessageProperties( + content_encoding=encoding, content_type=content_type + ) + message = Message( + body=json.dumps(payload).encode(), + properties=properties, + annotations={ + common_parser.DEVICE_ID_IDENTIFIER: device_id.encode(), + common_parser.INTERFACE_NAME_IDENTIFIER: interface_name.encode(), + }, + application_properties=_encode_app_props(app_properties), + ) + args = CommonParserArguments( + properties=["all"], interface_name=interface_name, content_type=content_type + ) + parser = common_parser.CommonParser(message=message, common_parser_args=args) + + # act + parsed_msg = parser.parse_message() + + # verify + assert parsed_msg["event"]["payload"] == payload + assert parsed_msg["event"]["origin"] == device_id + device_identifier = str(common_parser.DEVICE_ID_IDENTIFIER, "utf8") + assert parsed_msg["event"]["annotations"][device_identifier] == device_id + + properties = parsed_msg["event"]["properties"] + assert properties["system"]["content_encoding"] == encoding + assert properties["system"]["content_type"] == content_type + assert properties["application"] == app_properties + + assert len(parser.issues_handler.get_all_issues()) == 0 + + def test_parse_message_bad_content_type_should_warn(self): + # setup + encoded_payload = json.dumps(self.payload).encode() + properties = MessageProperties(content_type=self.bad_content_type) + message = Message( + body=encoded_payload, + properties=properties, + annotations={common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + ) + args = CommonParserArguments() + parser = common_parser.CommonParser(message=message, common_parser_args=args) + + # act + parsed_msg = parser.parse_message() + + # verify + # since the content_encoding header is not present, just dump the raw payload + payload = str(encoded_payload, "utf8") + assert parsed_msg["event"]["payload"] == payload + + expected_details = strings.invalid_encoding_none_found() + _validate_issues(parser, Severity.error, 2, 1, [expected_details]) + + expected_details = strings.invalid_content_type(self.bad_content_type) + _validate_issues(parser, Severity.warning, 2, 1, [expected_details]) + + def test_parse_message_bad_encoding_should_fail(self): + # setup + properties = MessageProperties( + content_encoding=self.bad_encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.payload).encode(self.bad_encoding), + properties=properties, + annotations={common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + ) + args = CommonParserArguments() + parser = common_parser.CommonParser(message=message, common_parser_args=args) + + # act + parser.parse_message() + + expected_details = strings.invalid_encoding(self.bad_encoding) + _validate_issues(parser, Severity.error, 1, 1, [expected_details]) + + def test_parse_message_bad_json_should_fail(self): + # setup + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=self.bad_payload.encode(), + properties=properties, + annotations={common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + ) + args = CommonParserArguments() + parser = common_parser.CommonParser(message=message, common_parser_args=args) + + # act + parsed_msg = parser.parse_message() + + # verify + # parsing should attempt to place raw payload into result even if parsing fails + assert parsed_msg["event"]["payload"] == self.bad_payload + + expected_details = strings.invalid_json() + _validate_issues(parser, Severity.error, 1, 1, [expected_details]) + + def test_parse_message_pnp_should_fail(self): + # setup + actual_interface_name = "actual_interface_name" + expected_interface_name = "expected_interface_name" + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.payload).encode(), + properties=properties, + annotations={ + common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode(), + common_parser.INTERFACE_NAME_IDENTIFIER: actual_interface_name.encode(), + }, + ) + args = CommonParserArguments(interface_name=expected_interface_name) + parser = common_parser.CommonParser(message=message, common_parser_args=args) + + # act + parsed_msg = parser.parse_message() + + # verify + # all the items should still be parsed and available, but we should have an error + assert parsed_msg["event"]["payload"] == self.payload + assert parsed_msg["event"]["origin"] == self.device_id + assert parsed_msg["event"]["interface"] == actual_interface_name + + expected_details = strings.invalid_interface_name_mismatch( + expected_interface_name, actual_interface_name + ) + _validate_issues(parser, Severity.warning, 1, 1, [expected_details]) + + +class TestCentralParser: + device_id = "some-device-id" + payload = {"String": "someValue"} + encoding = "UTF-8" + content_type = "application/json" + app_properties = {"appPropsKey": "appPropsValue"} + + bad_encoding = "ascii" + bad_payload = "bad-payload" + bad_field_name = {"violates-regex": "someValue"} + bad_content_type = "bad-content-type" + + bad_dcm_payload = {"temperature": "someValue"} + type_mismatch_payload = {"Bool": "someValue"} + + def test_parse_message_bad_field_name_should_fail(self): + # setup + device_template = self._get_template() + + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.bad_field_name).encode(), + properties=properties, + annotations={common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + ) + args = CommonParserArguments() + parser = self._create_parser( + device_template=device_template, message=message, args=args + ) + + # act + parsed_msg = parser.parse_message() + schema = parser._extract_template_schemas_from_template(device_template) + + # verify + # parsing should attempt to place raw payload into result even if parsing fails + assert parsed_msg["event"]["payload"] == self.bad_field_name + + expected_details_1 = strings.invalid_field_name( + list(self.bad_field_name.keys()) + ) + expected_details_2 = strings.invalid_field_name_mismatch_template( + list(self.bad_field_name.keys()), list(schema.keys()) + ) + _validate_issues( + parser, Severity.warning, 2, 2, [expected_details_1, expected_details_2] + ) + + def test_validate_against_template_should_fail(self): + # setup + device_template = self._get_template() + + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.bad_dcm_payload).encode(), + properties=properties, + annotations={common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + application_properties=_encode_app_props(self.app_properties), + ) + args = CommonParserArguments(properties=["all"]) + parser = self._create_parser( + device_template=device_template, message=message, args=args + ) + + # act + parsed_msg = parser.parse_message() + schema = parser._extract_template_schemas_from_template(device_template) + + schema = parser._extract_template_schemas_from_template(device_template) + + # verify + assert parsed_msg["event"]["payload"] == self.bad_dcm_payload + assert parsed_msg["event"]["origin"] == self.device_id + device_identifier = str(common_parser.DEVICE_ID_IDENTIFIER, "utf8") + assert parsed_msg["event"]["annotations"][device_identifier] == self.device_id + + properties = parsed_msg["event"]["properties"] + assert properties["system"]["content_encoding"] == self.encoding + assert properties["system"]["content_type"] == self.content_type + assert properties["application"] == self.app_properties + + expected_details = strings.invalid_field_name_mismatch_template( + list(self.bad_dcm_payload.keys()), list(schema.keys()) + ) + + _validate_issues(parser, Severity.warning, 1, 1, [expected_details]) + + def test_validate_against_bad_template_should_not_throw(self): + # setup + device_template = "an_unparseable_template" + + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.bad_dcm_payload).encode(), + properties=properties, + annotations={common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + application_properties=_encode_app_props(self.app_properties), + ) + args = CommonParserArguments(properties=["all"]) + parser = self._create_parser( + device_template=device_template, message=message, args=args + ) + + # act + parsed_msg = parser.parse_message() + + # verify + assert parsed_msg["event"]["payload"] == self.bad_dcm_payload + assert parsed_msg["event"]["origin"] == self.device_id + + expected_details = strings.invalid_template_extract_schema_failed( + device_template + ) + + _validate_issues(parser, Severity.error, 1, 1, [expected_details]) + + def test_type_mismatch_should_error(self): + # setup + device_template = self._get_template() + + properties = MessageProperties( + content_encoding=self.encoding, content_type=self.content_type + ) + message = Message( + body=json.dumps(self.type_mismatch_payload).encode(), + properties=properties, + annotations={common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + application_properties=_encode_app_props(self.app_properties), + ) + args = CommonParserArguments(properties=["all"]) + parser = self._create_parser( + device_template=device_template, message=message, args=args + ) + + # act + parsed_msg = parser.parse_message() + + # verify + assert parsed_msg["event"]["payload"] == self.type_mismatch_payload + assert parsed_msg["event"]["origin"] == self.device_id + assert parsed_msg["event"]["properties"]["application"] == self.app_properties + + field_name = list(self.type_mismatch_payload.keys())[0] + data = list(self.type_mismatch_payload.values())[0] + data_type = "boolean" + expected_details = strings.invalid_primitive_schema_mismatch_template( + field_name, data_type, data + ) + _validate_issues(parser, Severity.warning, 1, 1, [expected_details]) + + def _get_template(self): + return load_json(FileNames.central_device_template_file) + + def _create_parser( + self, device_template: dict, message: Message, args: CommonParserArguments + ): + provider = CentralDeviceProvider(cmd=None, app_id=None) + provider.get_device_template_by_device_id = mock.MagicMock( + return_value=device_template + ) + return central_parser.CentralParser( + message=message, central_device_provider=provider, common_parser_args=args + ) From f5678af1381731f2d734c3c1fbb0cca3cb8177d6 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Thu, 14 May 2020 18:59:35 -0700 Subject: [PATCH 032/179] Change error severities, remove dynamic validation logic (#183) * Updated error levels to new severities, removed calls to dynamic validation * Always add interface name if present, added filtering logic --- azext_iot/monitor/handlers/common_handler.py | 13 ++++ azext_iot/monitor/models/arguments.py | 5 +- azext_iot/monitor/parsers/central_parser.py | 7 ++- azext_iot/monitor/parsers/common_parser.py | 37 ++++------- azext_iot/monitor/parsers/strings.py | 53 +++++++++------- azext_iot/operations/hub.py | 3 +- azext_iot/tests/test_monitor_parsers_unit.py | 64 ++++---------------- 7 files changed, 78 insertions(+), 104 deletions(-) diff --git a/azext_iot/monitor/handlers/common_handler.py b/azext_iot/monitor/handlers/common_handler.py index 0e083c6d0..44903e99a 100644 --- a/azext_iot/monitor/handlers/common_handler.py +++ b/azext_iot/monitor/handlers/common_handler.py @@ -27,6 +27,9 @@ def parse_message(self, message): if not self._should_process_device(parser.device_id): return + if not self._should_process_interface(parser.interface_name): + return + result = parser.parse_message() if self._common_handler_args.output.lower() == "json": @@ -57,3 +60,13 @@ def _should_process_device(self, device_id): return False return True + + def _should_process_interface(self, interface_name): + expected_interface_name = self._common_handler_args.interface_name + + # if no filter is specified, then process all interfaces + if not expected_interface_name: + return True + + # only process if the expected and actual interface name match + return expected_interface_name == interface_name diff --git a/azext_iot/monitor/models/arguments.py b/azext_iot/monitor/models/arguments.py index ffac7a448..adfa297ea 100644 --- a/azext_iot/monitor/models/arguments.py +++ b/azext_iot/monitor/models/arguments.py @@ -35,10 +35,9 @@ def __init__( class CommonParserArguments: def __init__( - self, properties: list = None, interface_name="", content_type="", + self, properties: list = None, content_type="", ): self.properties = properties or [] - self.interface_name = interface_name or "" self.content_type = content_type or "" @@ -49,10 +48,12 @@ def __init__( common_parser_args: CommonParserArguments, devices: list = None, device_id="", + interface_name="", ): self.output = output self.devices = devices or [] self.device_id = device_id or "" + self.interface_name = interface_name or "" self.common_parser_args = common_parser_args diff --git a/azext_iot/monitor/parsers/central_parser.py b/azext_iot/monitor/parsers/central_parser.py index 76bb01512..6c293130d 100644 --- a/azext_iot/monitor/parsers/central_parser.py +++ b/azext_iot/monitor/parsers/central_parser.py @@ -47,7 +47,8 @@ def parse_message(self) -> dict: self._perform_static_validations(payload=payload) - self._perform_dynamic_validations(payload=payload) + # disable dynamic validations until Microservices work is figured out + # self._perform_dynamic_validations(payload=payload) return parsed_message @@ -73,7 +74,7 @@ def _validate_field_names(self, payload: dict): ] if invalid_field_names: details = strings.invalid_field_name(invalid_field_names) - self._add_issue(severity=Severity.warning, details=details) + self._add_issue(severity=Severity.error, details=details) # Dynamic validations should need data external to the payload # e.g. device template @@ -142,7 +143,7 @@ def _validate_payload_against_schema( details = strings.invalid_primitive_schema_mismatch_template( name, expected_type, value ) - self._add_central_issue(severity=Severity.warning, details=details) + self._add_central_issue(severity=Severity.error, details=details) if name_miss: details = strings.invalid_field_name_mismatch_template( diff --git a/azext_iot/monitor/parsers/common_parser.py b/azext_iot/monitor/parsers/common_parser.py index 0ebd3c732..fa9385a92 100644 --- a/azext_iot/monitor/parsers/common_parser.py +++ b/azext_iot/monitor/parsers/common_parser.py @@ -26,15 +26,16 @@ def __init__(self, message: Message, common_parser_args: CommonParserArguments): self._common_parser_args = common_parser_args self._message = message self.device_id = self._parse_device_id(message) + self.interface_name = self._parse_interface_name(message) def parse_message(self) -> dict: message = self._message properties = self._common_parser_args.properties content_type = self._common_parser_args.content_type - interface_name = self._common_parser_args.interface_name event = {} event["origin"] = self.device_id + event["interface"] = self.interface_name if not properties: properties = [] # guard against None being passed in @@ -45,11 +46,6 @@ def parse_message(self) -> dict: content_type = self._parse_content_type(content_type, system_properties) - if interface_name: - message_interface_name = self._parse_interface_name(message, interface_name) - - event["interface"] = message_interface_name - if properties: event["properties"] = {} @@ -88,7 +84,7 @@ def _parse_device_id(self, message: Message) -> str: self._add_issue(severity=Severity.error, details=details) return "" - def _parse_interface_name(self, message: Message, expected_interface_name) -> str: + def _parse_interface_name(self, message: Message) -> str: actual_interface_name = "" try: @@ -96,14 +92,7 @@ def _parse_interface_name(self, message: Message, expected_interface_name) -> st message.annotations.get(INTERFACE_NAME_IDENTIFIER), "utf8" ) except Exception: - details = strings.invalid_interface_name_not_found() - self._add_issue(severity=Severity.error, details=details) - - if expected_interface_name != actual_interface_name: - details = strings.invalid_interface_name_mismatch( - expected_interface_name, actual_interface_name - ) - self._add_issue(severity=Severity.warning, details=details) + pass return actual_interface_name @@ -112,7 +101,7 @@ def _parse_system_properties(self, message: Message): return unicode_binary_map(parse_entity(message.properties, True)) except Exception: details = strings.invalid_system_properties() - self._add_issue(severity=Severity.error, details=details) + self._add_issue(severity=Severity.warning, details=details) return {} def _parse_content_encoding(self, message: Message, system_properties) -> str: @@ -123,12 +112,12 @@ def _parse_content_encoding(self, message: Message, system_properties) -> str: if not content_encoding: details = strings.invalid_encoding_none_found() - self._add_issue(severity=Severity.error, details=details) + self._add_issue(severity=Severity.warning, details=details) return None if "utf-8" not in content_encoding.lower(): details = strings.invalid_encoding(content_encoding.lower()) - self._add_issue(severity=Severity.error, details=details) + self._add_issue(severity=Severity.warning, details=details) return None return content_encoding @@ -141,8 +130,8 @@ def _parse_content_type(self, content_type_hint, system_properties) -> str: content_type = system_properties["content_type"] if not content_type: - details = strings.invalid_encoding_missing(system_properties) - self._add_issue(severity=Severity.error, details=details) + details = strings.invalid_encoding_missing() + self._add_issue(severity=Severity.warning, details=details) return content_type @@ -150,16 +139,16 @@ def _parse_annotations(self, message: Message): try: return unicode_binary_map(message.annotations) except Exception: - details = strings.invalid_annotations(message) - self._add_issue(severity=Severity.error, details=details) + details = strings.invalid_annotations() + self._add_issue(severity=Severity.warning, details=details) return {} def _parse_application_properties(self, message: Message): try: return unicode_binary_map(message.application_properties) except Exception: - details = strings.invalid_application_properties(message) - self._add_issue(severity=Severity.error, details=details) + details = strings.invalid_application_properties() + self._add_issue(severity=Severity.warning, details=details) return {} def _parse_payload(self, message: Message, content_type): diff --git a/azext_iot/monitor/parsers/strings.py b/azext_iot/monitor/parsers/strings.py index cbd443e65..7962fec65 100644 --- a/azext_iot/monitor/parsers/strings.py +++ b/azext_iot/monitor/parsers/strings.py @@ -10,21 +10,21 @@ SUPPORTED_MESSAGE_HEADERS = [] -def unknown_device_id(): - return "Device ID not found in message".format() +def unknown_device_id(): # error + return "Device ID not found in message" -def invalid_json(): - return "Invalid JSON format.".format() +def invalid_json(): # error + return "Invalid JSON format." -def invalid_encoding(encoding: str): +def invalid_encoding(encoding: str): # warning return "Encoding type '{}' is not supported. Expected encoding '{}'.".format( encoding, SUPPORTED_ENCODING ) -def invalid_field_name(invalid_field_names: list): +def invalid_field_name(invalid_field_names: list): # error return ( "Invalid characters detected. Invalid field names: '{}'. " "Allowed characters: {}." @@ -39,8 +39,8 @@ def invalid_non_pnp_field_name_duplicate(): raise NotImplementedError() -def invalid_content_type(content_type: str): - return "Content type '{}' is not supported. Expected Content type: {}.".format( +def invalid_content_type(content_type: str): # warning + return "Content type '{}' is not supported. Expected Content type is '{}'.".format( content_type, SUPPORTED_CONTENT_TYPE ) @@ -53,6 +53,7 @@ def invalid_custom_headers(): ).format(SUPPORTED_MESSAGE_HEADERS) +# warning def invalid_field_name_mismatch_template( unmodeled_capabilities: list, modeled_capabilities: list ): @@ -63,6 +64,7 @@ def invalid_field_name_mismatch_template( ).format(unmodeled_capabilities, modeled_capabilities) +# error def invalid_primitive_schema_mismatch_template(field_name: str, data_type: str, data): return ( "Datatype of field '{}' does not match the datatype '{}'. Data '{}'. " @@ -72,10 +74,12 @@ def invalid_primitive_schema_mismatch_template(field_name: str, data_type: str, ) +# to remove def invalid_interface_name_not_found(): return "Interface name not found." +# to remove def invalid_interface_name_mismatch( expected_interface_name: str, actual_interface_name: str ): @@ -84,33 +88,38 @@ def invalid_interface_name_mismatch( ) +# warning; downgrade to info if needed def invalid_system_properties(): - return "Failed to parse system properties.".format() + return "Failed to parse system properties." +# warning def invalid_encoding_none_found(): - return "No encoding found. Expected encoding 'utf-8' to be present in message header.".format() + return ( + "No encoding found. Expected encoding 'utf-8' to be present in message header." + ) -def invalid_encoding_missing(system_properties: dict): - return "Content type not found in system properties. System properties: {}.".format( - system_properties - ) +# warning +def invalid_encoding_missing(): + return "Content type not found in system properties." -def invalid_annotations(message): - return "Unable to decode message.annotations: {}.".format(message.annotations) +# warning +def invalid_annotations(): + return "Unable to decode message.annotations." -def invalid_application_properties(message): - return "Unable to decode message.application_properties: {}.".format( - message.application_properties - ) +# warning +def invalid_application_properties(): + return "Unable to decode message.application_properties." +# error def device_template_not_found(error: Exception): - return "Error retrieving template '{}'".format(error) + return "Error retrieving template '{}'. Please try again later.".format(error) +# error def invalid_template_extract_schema_failed(template: dict): - return "Unable to extract device schema from template '{}'".format(template) + return "Unable to extract device schema from template '{}'.".format(template) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 35499b7ae..f124275a6 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -2246,13 +2246,14 @@ def _iot_hub_monitor_events( on_start_string = generate_on_start_string(device_id=device_id) parser_args = CommonParserArguments( - properties=properties, interface_name=interface_name, content_type=content_type + properties=properties, content_type=content_type ) handler_args = CommonHandlerArguments( output=output, common_parser_args=parser_args, devices=device_ids, device_id=device_id, + interface_name=interface_name, ) handler = CommonHandler(handler_args) diff --git a/azext_iot/tests/test_monitor_parsers_unit.py b/azext_iot/tests/test_monitor_parsers_unit.py index e2c654824..ed66c4642 100644 --- a/azext_iot/tests/test_monitor_parsers_unit.py +++ b/azext_iot/tests/test_monitor_parsers_unit.py @@ -114,9 +114,7 @@ def test_parse_message_should_succeed( }, application_properties=_encode_app_props(app_properties), ) - args = CommonParserArguments( - properties=["all"], interface_name=interface_name, content_type=content_type - ) + args = CommonParserArguments(properties=["all"], content_type=content_type) parser = common_parser.CommonParser(message=message, common_parser_args=args) # act @@ -155,13 +153,13 @@ def test_parse_message_bad_content_type_should_warn(self): payload = str(encoded_payload, "utf8") assert parsed_msg["event"]["payload"] == payload - expected_details = strings.invalid_encoding_none_found() - _validate_issues(parser, Severity.error, 2, 1, [expected_details]) - - expected_details = strings.invalid_content_type(self.bad_content_type) - _validate_issues(parser, Severity.warning, 2, 1, [expected_details]) + expected_details_1 = strings.invalid_encoding_none_found() + expected_details_2 = strings.invalid_content_type(self.bad_content_type) + _validate_issues( + parser, Severity.warning, 2, 2, [expected_details_1, expected_details_2] + ) - def test_parse_message_bad_encoding_should_fail(self): + def test_parse_message_bad_encoding_should_warn(self): # setup properties = MessageProperties( content_encoding=self.bad_encoding, content_type=self.content_type @@ -178,7 +176,7 @@ def test_parse_message_bad_encoding_should_fail(self): parser.parse_message() expected_details = strings.invalid_encoding(self.bad_encoding) - _validate_issues(parser, Severity.error, 1, 1, [expected_details]) + _validate_issues(parser, Severity.warning, 1, 1, [expected_details]) def test_parse_message_bad_json_should_fail(self): # setup @@ -203,38 +201,6 @@ def test_parse_message_bad_json_should_fail(self): expected_details = strings.invalid_json() _validate_issues(parser, Severity.error, 1, 1, [expected_details]) - def test_parse_message_pnp_should_fail(self): - # setup - actual_interface_name = "actual_interface_name" - expected_interface_name = "expected_interface_name" - properties = MessageProperties( - content_encoding=self.encoding, content_type=self.content_type - ) - message = Message( - body=json.dumps(self.payload).encode(), - properties=properties, - annotations={ - common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode(), - common_parser.INTERFACE_NAME_IDENTIFIER: actual_interface_name.encode(), - }, - ) - args = CommonParserArguments(interface_name=expected_interface_name) - parser = common_parser.CommonParser(message=message, common_parser_args=args) - - # act - parsed_msg = parser.parse_message() - - # verify - # all the items should still be parsed and available, but we should have an error - assert parsed_msg["event"]["payload"] == self.payload - assert parsed_msg["event"]["origin"] == self.device_id - assert parsed_msg["event"]["interface"] == actual_interface_name - - expected_details = strings.invalid_interface_name_mismatch( - expected_interface_name, actual_interface_name - ) - _validate_issues(parser, Severity.warning, 1, 1, [expected_details]) - class TestCentralParser: device_id = "some-device-id" @@ -270,7 +236,6 @@ def test_parse_message_bad_field_name_should_fail(self): # act parsed_msg = parser.parse_message() - schema = parser._extract_template_schemas_from_template(device_template) # verify # parsing should attempt to place raw payload into result even if parsing fails @@ -279,14 +244,9 @@ def test_parse_message_bad_field_name_should_fail(self): expected_details_1 = strings.invalid_field_name( list(self.bad_field_name.keys()) ) - expected_details_2 = strings.invalid_field_name_mismatch_template( - list(self.bad_field_name.keys()), list(schema.keys()) - ) - _validate_issues( - parser, Severity.warning, 2, 2, [expected_details_1, expected_details_2] - ) + _validate_issues(parser, Severity.error, 1, 1, [expected_details_1]) - def test_validate_against_template_should_fail(self): + def xtest_validate_against_template_should_fail(self): # setup device_template = self._get_template() @@ -327,7 +287,7 @@ def test_validate_against_template_should_fail(self): _validate_issues(parser, Severity.warning, 1, 1, [expected_details]) - def test_validate_against_bad_template_should_not_throw(self): + def xtest_validate_against_bad_template_should_not_throw(self): # setup device_template = "an_unparseable_template" @@ -358,7 +318,7 @@ def test_validate_against_bad_template_should_not_throw(self): _validate_issues(parser, Severity.error, 1, 1, [expected_details]) - def test_type_mismatch_should_error(self): + def xtest_type_mismatch_should_error(self): # setup device_template = self._get_template() From c3795b08aee52ae389c64d4660c9cd91dda1ca9a Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 18 May 2020 15:17:38 -0700 Subject: [PATCH 033/179] Update README.md --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 84a6649d9..a2696419e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,23 @@ 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 + +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' +``` + +The resolution is to remove the deprecated `azure-cli-iot-ext` and install any version of the `azure-iot` extension. + + ## Commands Please refer to the official `az iot` reference on [Microsoft Docs](https://docs.microsoft.com/en-us/cli/azure/ext/azure-iot/iot?view=azure-cli-latest) for a complete list of supported commands. You can also find IoT CLI usage tips on the [wiki](https://github.com/Azure/azure-iot-cli-extension/wiki/Tips). From 9aa3f5f7cd2df379f1321214fbff1575a8ae1a0e Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Tue, 19 May 2020 11:10:47 -0700 Subject: [PATCH 034/179] Try parse payload as JSON even if its not JSON (#185) * Try parse payload as JSON even if its not JSON * Will only treat payload as JSON if application/json is specified * Fix lint issue --- azext_iot/central/commands_monitor.py | 8 ++- azext_iot/monitor/parsers/common_parser.py | 66 +++++++++++--------- azext_iot/monitor/parsers/strings.py | 6 +- azext_iot/tests/test_monitor_parsers_unit.py | 42 +++++++++++-- 4 files changed, 83 insertions(+), 39 deletions(-) diff --git a/azext_iot/central/commands_monitor.py b/azext_iot/central/commands_monitor.py index b33168ceb..60cfe4b66 100644 --- a/azext_iot/central/commands_monitor.py +++ b/azext_iot/central/commands_monitor.py @@ -39,7 +39,9 @@ def validate_messages( repair=repair, yes=yes, ) - common_parser_args = CommonParserArguments(properties=telemetry_args.properties) + common_parser_args = CommonParserArguments( + properties=telemetry_args.properties, content_type="application/json" + ) common_handler_args = CommonHandlerArguments( output=telemetry_args.output, common_parser_args=common_parser_args, @@ -82,7 +84,9 @@ def monitor_events( repair=repair, yes=yes, ) - common_parser_args = CommonParserArguments(properties=telemetry_args.properties) + common_parser_args = CommonParserArguments( + properties=telemetry_args.properties, content_type="application/json" + ) common_handler_args = CommonHandlerArguments( output=telemetry_args.output, common_parser_args=common_parser_args, diff --git a/azext_iot/monitor/parsers/common_parser.py b/azext_iot/monitor/parsers/common_parser.py index fa9385a92..527969a59 100644 --- a/azext_iot/monitor/parsers/common_parser.py +++ b/azext_iot/monitor/parsers/common_parser.py @@ -85,16 +85,12 @@ def _parse_device_id(self, message: Message) -> str: return "" def _parse_interface_name(self, message: Message) -> str: - actual_interface_name = "" - try: - actual_interface_name = str( - message.annotations.get(INTERFACE_NAME_IDENTIFIER), "utf8" - ) + return str(message.annotations.get(INTERFACE_NAME_IDENTIFIER), "utf8") except Exception: - pass - - return actual_interface_name + # a message not containing an interface name is expected for non-pnp devices + # so there's no "issue" to log here + return "" def _parse_system_properties(self, message: Message): try: @@ -122,18 +118,27 @@ def _parse_content_encoding(self, message: Message, system_properties) -> str: return content_encoding - def _parse_content_type(self, content_type_hint, system_properties) -> str: - content_type = "" - if content_type_hint: - content_type = content_type_hint - elif "content_type" in system_properties: - content_type = system_properties["content_type"] - - if not content_type: - details = strings.invalid_encoding_missing() + def _parse_content_type( + self, expected_content_type: str, system_properties: dict + ) -> str: + actual_content_type = system_properties.get("content_type", "") + + # Device data is not expected to be of a certain type + # Continue parsing per rules that the device is sending + if not expected_content_type: + return actual_content_type.lower() + + # Device is expected to send data in a certain format. + # Data from device implies the data is in an incorrect format. + # Log the issue, and continue parsing as if device is in expected format. + if actual_content_type.lower() != expected_content_type.lower(): + details = strings.content_type_mismatch( + actual_content_type, expected_content_type + ) self._add_issue(severity=Severity.warning, details=details) + return expected_content_type.lower() - return content_type + return actual_content_type def _parse_annotations(self, message: Message): try: @@ -158,16 +163,19 @@ def _parse_payload(self, message: Message, content_type): if data: payload = str(next(data), "utf8") - if "application/json" not in content_type.lower(): - details = strings.invalid_content_type(content_type.lower()) - self._add_issue(severity=Severity.warning, details=details) - else: - try: - regex = r"(\\r\\n)+|\\r+|\\n+" - payload_no_white_space = re.compile(regex).sub("", payload) - payload = json.loads(payload_no_white_space) - except Exception: - details = strings.invalid_json() - self._add_issue(severity=Severity.error, details=details) + if "application/json" in content_type.lower(): + return self._try_parse_json(payload) return payload + + def _try_parse_json(self, payload): + result = payload + try: + regex = r"(\\r\\n)+|\\r+|\\n+" + payload_no_white_space = re.compile(regex).sub("", payload) + result = json.loads(payload_no_white_space) + except Exception: + details = strings.invalid_json() + self._add_issue(severity=Severity.error, details=details) + + return result diff --git a/azext_iot/monitor/parsers/strings.py b/azext_iot/monitor/parsers/strings.py index 7962fec65..a241f2566 100644 --- a/azext_iot/monitor/parsers/strings.py +++ b/azext_iot/monitor/parsers/strings.py @@ -39,9 +39,11 @@ def invalid_non_pnp_field_name_duplicate(): raise NotImplementedError() -def invalid_content_type(content_type: str): # warning +def content_type_mismatch( + actual_content_type: str, expected_content_type: str +): # warning return "Content type '{}' is not supported. Expected Content type is '{}'.".format( - content_type, SUPPORTED_CONTENT_TYPE + actual_content_type, expected_content_type ) diff --git a/azext_iot/tests/test_monitor_parsers_unit.py b/azext_iot/tests/test_monitor_parsers_unit.py index ed66c4642..1ea29adc6 100644 --- a/azext_iot/tests/test_monitor_parsers_unit.py +++ b/azext_iot/tests/test_monitor_parsers_unit.py @@ -46,7 +46,7 @@ class TestCommonParser: content_type = "application/json" bad_encoding = "ascii" - bad_payload = "bad-payload" + bad_payload = "{bad-payload" bad_content_type = "bad-content-type" @pytest.mark.parametrize( @@ -142,7 +142,35 @@ def test_parse_message_bad_content_type_should_warn(self): properties=properties, annotations={common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, ) - args = CommonParserArguments() + args = CommonParserArguments(content_type="application/json") + parser = common_parser.CommonParser(message=message, common_parser_args=args) + + # act + parsed_msg = parser.parse_message() + + # verify + assert parsed_msg["event"]["payload"] == self.payload + + expected_details_1 = strings.invalid_encoding_none_found() + expected_details_2 = strings.content_type_mismatch( + self.bad_content_type, "application/json" + ) + _validate_issues( + parser, Severity.warning, 2, 2, [expected_details_1, expected_details_2], + ) + + def test_parse_bad_type_and_bad_payload_should_error(self): + # setup + encoded_payload = self.bad_payload.encode() + properties = MessageProperties( + content_type=self.bad_content_type, content_encoding=self.encoding + ) + message = Message( + body=encoded_payload, + properties=properties, + annotations={common_parser.DEVICE_ID_IDENTIFIER: self.device_id.encode()}, + ) + args = CommonParserArguments(content_type="application/json") parser = common_parser.CommonParser(message=message, common_parser_args=args) # act @@ -153,11 +181,13 @@ def test_parse_message_bad_content_type_should_warn(self): payload = str(encoded_payload, "utf8") assert parsed_msg["event"]["payload"] == payload - expected_details_1 = strings.invalid_encoding_none_found() - expected_details_2 = strings.invalid_content_type(self.bad_content_type) - _validate_issues( - parser, Severity.warning, 2, 2, [expected_details_1, expected_details_2] + expected_details_1 = strings.content_type_mismatch( + self.bad_content_type, "application/json" ) + _validate_issues(parser, Severity.warning, 2, 1, [expected_details_1]) + + expected_details_2 = strings.invalid_json() + _validate_issues(parser, Severity.error, 2, 1, [expected_details_2]) def test_parse_message_bad_encoding_should_warn(self): # setup From 993a8ea248dc3c22b975dd353b70d9ecfdaebfef Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 19 May 2020 14:16:16 -0700 Subject: [PATCH 035/179] Update CODEOWNERS --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f8f0642c7..9783bf9cb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,3 +9,6 @@ azext_iot/central/ @prbans # Monitor Code Owner(s) azext_iot/monitor/ @prbans @digimaun + +# Tests Code Owners +azext_iot/tests/ @prbans From 1f95c974154d33c117a4f7d608d6054dded75a7b Mon Sep 17 00:00:00 2001 From: valluriraj Date: Tue, 19 May 2020 14:30:13 -0700 Subject: [PATCH 036/179] fix registration-info (#187) * fix registration-info * address code review comments * address review comments * address review comments Co-authored-by: Raj valluri Co-authored-by: prbans <42591144+prbans@users.noreply.github.com> --- azext_iot/central/models/device.py | 11 +++++++++++ .../central/providers/device_provider.py | 19 +++++++++++-------- azext_iot/tests/test_iot_central_int.py | 18 +++++++++++++----- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/azext_iot/central/models/device.py b/azext_iot/central/models/device.py index b30670bf5..7bc02bd78 100644 --- a/azext_iot/central/models/device.py +++ b/azext_iot/central/models/device.py @@ -31,3 +31,14 @@ def _parse_device_status(self) -> DeviceStatus: return DeviceStatus.registered return DeviceStatus.provisioned + + def get_registration_info(self): + registration_info = { + "device_status": self.device_status.value, + "display_name": self.display_name, + "id": self.id, + "simulated": self.simulated, + "instance_of": self.instance_of, + } + + return registration_info diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 27dc2558e..5af51d6ec 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -181,7 +181,7 @@ def get_device_registration_info( if info: return info - device = self.get_device(device_id) + 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 @@ -192,22 +192,24 @@ def get_device_registration_info( id_scope=id_scope, key=key, device_id=device_id ) dps_state = self.dps_populate_essential_info(dps_state, device.device_status) + info = { "@device_id": device_id, "dps_state": dps_state, - "device_info": device, + "device_registration_info": device.get_registration_info(), } self._device_registration_info[device_id] = info return info - def dps_populate_essential_info(self, dps_info, device_status): + def dps_populate_essential_info(self, dps_info, device_status: DeviceStatus): error = { - "provisioned": "None.", - "registered": "Device is not yet provisioned.", - "blocked": "Device is blocked by admin.", - "unassociated": "Device does not have a valid template associated with it.", + 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 = { @@ -228,10 +230,11 @@ def get_all_registration_info( filtered_devices = real_devices if device_status: + requested_status = DeviceStatus(device_status) filtered_devices = [ device for device in real_devices - if device.device_status == device_status + if device.device_status == requested_status ] if len(devices) != len(filtered_devices): diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index fd9108069..0aca61a98 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -167,7 +167,7 @@ def test_central_device_registration_info(self): # since time taken for provisioning to complete is not known # we can only assert that the payload is populated, not anything specific beyond that - assert json_result["device_info"] is not None + assert json_result["device_registration_info"] is not None assert json_result["dps_state"] is not None def test_central_device_registration_info_filter_unassociated(self): @@ -187,10 +187,14 @@ def test_central_device_registration_info_filter_unassociated(self): device_info_results = [] json_result = result.get_output_in_json() for device in json_result: - device_info_results.append(device.get("device_info")) + device_info_results.append(device.get("device_registration_info")) for device in device_info_results: - assert device.get("deviceStatus") == device_status_expected + assert device.get("device_status") == device_status_expected + assert device.get("display_name") + assert device.get("id") + assert not device.get("simulated") + assert not device.get("instance_of") def test_central_device_registration_info_filter_registered(self): device_status_expected = "registered" @@ -210,10 +214,14 @@ def test_central_device_registration_info_filter_registered(self): device_info_results = [] json_result = result.get_output_in_json() for device in json_result: - device_info_results.append(device.get("device_info")) + device_info_results.append(device.get("device_registration_info")) for device in device_info_results: - assert device.get("deviceStatus") == device_status_expected + assert device.get("device_status") == device_status_expected + assert device.get("display_name") + assert device.get("id") + assert not device.get("simulated") + assert device.get("instance_of") def _create_device(self, **kwargs) -> (str, str): """ From 49ce906dfa2910d67eae4b4b7a1e2bf00f44fa95 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 20 May 2020 14:04:28 -0700 Subject: [PATCH 037/179] Update CODEOWNERS --- .github/CODEOWNERS | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9783bf9cb..1933a95f7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,9 @@ azext_iot/central/ @prbans # Monitor Code Owner(s) -azext_iot/monitor/ @prbans @digimaun +azext_iot/monitor/ @prbans # Tests Code Owners -azext_iot/tests/ @prbans +azext_iot/tests/central/ @prbans +azext_iot/tests/test_iot_central_int.py @prbans +azext_iot/tests/test_iot_central_unit.py @prbans From eb24590fde89e180e49eda803dcc1d82b77c597b Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 20 May 2020 14:11:47 -0700 Subject: [PATCH 038/179] Update CODEOWNERS --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1933a95f7..d4870c31d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,3 +14,5 @@ azext_iot/monitor/ @prbans azext_iot/tests/central/ @prbans azext_iot/tests/test_iot_central_int.py @prbans azext_iot/tests/test_iot_central_unit.py @prbans +azext_iot/tests/test_monitor_parsers_unit.py @prbans +azext_iot/tests/test_uamqp_import.py @prbans From 30d8f4a1140cc9b48324561d65ba9dc21f70bae2 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Wed, 20 May 2020 18:48:24 -0700 Subject: [PATCH 039/179] Adds authentication-type parameters to device-identity import and export commands (#189) * Updated device-identity import/export commands to allow authenticationType, updated help * Conditional statements for device import/export depending on SDK version / support for Request objects * Moved identity-tests to optional / skipped fixture * Enhanced SDK version check * Implemented auth-type as enum and converted 'identityBased' and 'keyBased' params to 'identity' and 'key' * Address PR feedback Co-authored-by: Ryan Kelly Co-authored-by: Paymaun --- CONTRIBUTING.md | 3 ++ azext_iot/_params.py | 24 +++++++-- azext_iot/common/shared.py | 9 ++++ azext_iot/common/utility.py | 5 ++ azext_iot/operations/hub.py | 66 ++++++++++++++++++++++-- azext_iot/tests/test_iot_ext_int.py | 24 ++++++++- azext_iot/tests/test_iot_utility_unit.py | 17 ++++++ pytest.ini.example | 1 + 8 files changed, 140 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e59ca2693..75dccddae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -130,10 +130,13 @@ You can either manually set the environment variables or use the `pytest.ini.exa azext_iot_testhub_cs="IoT Hub Connection String" azext_iot_testdps="IoT Hub DPS Name" azext_iot_teststorageuri="Blob Container SAS Uri" + azext_iot_testidentity=True ``` `azext_iot_teststorageuri` is optional and only required when you want to test device export and file upload functionality. You can generate a SAS Uri for your Blob container using the [Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/). You must also configure your IoT Hub's File Upload storage container via the Azure Portal for this test to pass. +`azext_iot_testidentity` is optional and only required when you want to test Identity-Based device export and file upload functionality. You must ensure that your IoT Hub has been created with a [Managed Service Identity](https://docs.microsoft.com/en-us/azure/iot-hub/virtual-network-support#create-an-iot-hub-with-managed-service-identity). Also, you must ensure that your hub has been granted the `Storage Blob Data Contributor` role on your storage account, and it is configured as your IoT Hub's File Upload storage container as described above. + ##### IoT Hub Execute the following command to run the IoT Hub integration tests: diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 10b73acba..9099e62f1 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -31,6 +31,7 @@ JobType, JobCreateType, JobStatusType, + AuthenticationType, ) from azext_iot._validators import mode2_iot_login_handler from azext_iot.assets.user_messages import info_param_properties_device @@ -373,9 +374,10 @@ def load_arguments(self, _): context.argument( "blob_container_uri", options_list=["--blob-container-uri", "--bcu"], - help="Blob Shared Access Signature URI with write access to " + 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.", + "job and the results. Note: when using Identity-based authentication, you must supply an " + "https:// URI.", ) context.argument( "include_keys", @@ -384,6 +386,12 @@ def load_arguments(self, _): help="If set, keys are exported normally. Otherwise, keys are " "set to null in export output.", ) + context.argument( + "storage_authentication_type", + options_list=["--auth-type", "--storage-authentication-type"], + arg_type=get_enum_type(AuthenticationType), + help="Authentication type for communicating with the storage container.", + ) with self.argument_context("iot hub device-identity import") as context: context.argument( @@ -391,14 +399,22 @@ def load_arguments(self, _): options_list=["--input-blob-container-uri", "--ibcu"], 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 ", + "the identity registry. Note: when using Identity-based authentication, you must supply an " + "https:// URI", ) context.argument( "output_blob_container_uri", options_list=["--output-blob-container-uri", "--obcu"], 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.", + "the job and the results. Note: when using Identity-based authentication, you must supply an " + "https:// URI", + ) + context.argument( + "storage_authentication_type", + options_list=["--auth-type", "--storage-authentication-type"], + 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: diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index cb28783ee..03d683633 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -214,3 +214,12 @@ class JobStatusType(Enum): cancelled = "cancelled" scheduled = "scheduled" queued = "queued" + + +class AuthenticationType(Enum): + """ + Route or endpoint authentication mechanism. + """ + + keyBased = "key" + identityBased = "identity" diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index dafd84391..92dc6b727 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -462,3 +462,8 @@ def is_iso8601_time(self, to_validate: str) -> bool: return bool(isodate.parse_time(to_validate)) except Exception: return False + + +def ensure_min_version(cur_ver, min_ver): + from pkg_resources._vendor.packaging import version + return version.parse(cur_ver) >= version.parse(min_ver) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index f124275a6..89f5f7a7b 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -27,6 +27,7 @@ unpack_msrest_error, init_monitoring, process_json_arg, + ensure_min_version, ) from azext_iot._factory import SdkResolver, CloudError from azext_iot.operations.generic import _execute_query, _process_top @@ -2041,14 +2042,45 @@ def _handle_c2d_msg(target, device_id, receive_settle): def iot_device_export( - cmd, hub_name, blob_container_uri, include_keys=False, resource_group_name=None + cmd, + hub_name, + blob_container_uri, + include_keys=False, + storage_authentication_type=None, + resource_group_name=None, ): from azext_iot._factory import iot_hub_service_factory + from azure.mgmt.iothub import __version__ as iot_sdk_version client = iot_hub_service_factory(cmd.cli_ctx) target = get_iot_hub_connection_string(client, hub_name, resource_group_name) + + if ensure_min_version(iot_sdk_version, "0.12.0"): + from azure.mgmt.iothub.models import ExportDevicesRequest + from azext_iot.common.shared import AuthenticationType + + storage_authentication_type = ( + AuthenticationType(storage_authentication_type).name + 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, + ) + return client.export_devices( + target["resourcegroup"], hub_name, export_devices_parameters=export_request, + ) + if storage_authentication_type: + raise CLIError( + "Device export authentication-type properties require a dependency of azure-mgmt-iothub>=0.12.0" + ) return client.export_devices( - target["resourcegroup"], hub_name, blob_container_uri, not include_keys + target["resourcegroup"], + hub_name, + export_blob_container_uri=blob_container_uri, + exclude_keys=not include_keys, ) @@ -2057,17 +2089,43 @@ def iot_device_import( hub_name, input_blob_container_uri, output_blob_container_uri, + storage_authentication_type=None, resource_group_name=None, ): from azext_iot._factory import iot_hub_service_factory + from azure.mgmt.iothub import __version__ as iot_sdk_version client = iot_hub_service_factory(cmd.cli_ctx) target = get_iot_hub_connection_string(client, hub_name, resource_group_name) + + if ensure_min_version(iot_sdk_version, "0.12.0"): + from azure.mgmt.iothub.models import ImportDevicesRequest + from azext_iot.common.shared import AuthenticationType + + storage_authentication_type = ( + AuthenticationType(storage_authentication_type).name + 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, + input_blob_name=None, + output_blob_name=None, + authentication_type=storage_authentication_type, + ) + return client.import_devices( + target["resourcegroup"], hub_name, import_devices_parameters=import_request, + ) + if storage_authentication_type: + raise CLIError( + "Device import authentication-type properties require a dependency of azure-mgmt-iothub>=0.12.0" + ) return client.import_devices( target["resourcegroup"], hub_name, - input_blob_container_uri, - output_blob_container_uri, + input_blob_container_uri=input_blob_container_uri, + output_blob_container_uri=output_blob_container_uri, ) diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 77bdec996..e3914ffe0 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -13,7 +13,7 @@ from .settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC from azext_iot.constants import DEVICE_DEVICESCOPE_PREFIX -opt_env_set = ["azext_iot_teststorageuri"] +opt_env_set = ["azext_iot_teststorageuri", "azext_iot_testidentity"] settings = DynamoSettings( req_env_set=ENV_SET_TEST_IOTHUB_BASIC, opt_env_set=opt_env_set @@ -27,6 +27,11 @@ # 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 to have configured your IoT Hub and Storage Account before running. +LIVE_IDENTITY = settings.env.azext_iot_testidentity + LIVE_CONSUMER_GROUPS = ["test1", "test2", "test3"] CWD = os.path.dirname(os.path.abspath(__file__)) @@ -1253,6 +1258,23 @@ def test_storage(self): ], ) + @pytest.mark.skipif( + not LIVE_IDENTITY, reason="azext_iot_testidentity env var not set" + ) + def test_identity_storage(self): + # identity-based device-identity export + self.cmd( + 'iot hub device-identity export -n {} --bcu "{}" --auth-type {}'.format( + LIVE_HUB, LIVE_STORAGE, "identity" + ), + checks=[ + self.check("outputBlobContainerUri", LIVE_STORAGE), + self.check("failureReason", None), + self.check("type", "export"), + self.exists("jobId"), + ], + ) + class TestIoTEdgeOffline(IoTLiveScenarioTest): def __init__(self, test_case): diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index 194e73572..afffce0c6 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -16,6 +16,7 @@ read_file_content, logger, ISO8601Validator, + ensure_min_version, ) from azext_iot.common.deps import ensure_uamqp from azext_iot.constants import EVENT_LIB, EXTENSION_NAME @@ -345,3 +346,19 @@ def test_is_iso8601_duration_fail(self, to_validate): def test_is_iso8601_time_fail(self, to_validate): result = self.validator.is_iso8601_time(to_validate) assert not result + + +class TestVersionComparison: + @pytest.mark.parametrize( + "current, minimum, expected", + [ + ("1.0", "2.0", False), + ("1.8.7", "1.8.6.4", True), + ("1.0+a", "1.0", True), + ("1.0+a", "1.0+b", False), + ("1.0", "1.0", True), + ("2.0.1.9", "2.0.6", False), + ], + ) + def test_ensure_min_version(self, current, minimum, expected): + assert ensure_min_version(current, minimum) == expected diff --git a/pytest.ini.example b/pytest.ini.example index ad6dca94e..bad0c38df 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -18,6 +18,7 @@ env = azext_iot_testhub_cs= azext_iot_testdps= azext_iot_teststorageuri= + azext_iot_testidentity= azext_pnp_endpoint= azext_pnp_repository= azext_pnp_cs= From 1f38080168b51bfb628a6038646bcb4fa07a4e0b Mon Sep 17 00:00:00 2001 From: valluriraj Date: Thu, 21 May 2020 14:13:42 -0700 Subject: [PATCH 040/179] =?UTF-8?q?Remove=20support=20for=20filtering=20by?= =?UTF-8?q?=20device=20status.=20Device=20Id=20is=20a=20mandato=E2=80=A6?= =?UTF-8?q?=20(#191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove support for filtering by device status. Device Id is a mandatory field * lint fixes * address review comments Co-authored-by: Raj valluri --- azext_iot/central/_help.py | 10 +-- azext_iot/central/commands_device.py | 15 +--- azext_iot/central/params.py | 10 +-- .../central/providers/device_provider.py | 42 ++------- azext_iot/tests/test_iot_central_int.py | 90 ++++++++++--------- 5 files changed, 60 insertions(+), 107 deletions(-) diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index ef2db7516..7ad8b422c 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -111,22 +111,16 @@ def _load_central_devices_help(): "iot central app device registration-info" ] = """ type: command - short-summary: Get registration info on device(s) from IoT Central + short-summary: Get registration info for a given device in IoT Central long-summary: | Note: This command can take a significant amount of time to return if no device id is specified and your app contains a lot of devices examples: - - name: Get registration info on all devices. This command may take a long time to complete execution. - text: > - az iot central app device registration-info - --app-id {appid} - - name: Get registration info on specified device text: > az iot central app device registration-info - --app-id {appid} - --device-id {deviceid} + --app-id {appid} --device-id {deviceid} """ diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py index 2dad7c2cc..5b80b122e 100644 --- a/azext_iot/central/commands_device.py +++ b/azext_iot/central/commands_device.py @@ -63,21 +63,10 @@ def delete_device( def registration_info( - cmd, - app_id: str, - device_id=None, - token=None, - central_dns_suffix="azureiotcentral.com", - device_status=None, + cmd, app_id: str, device_id, token=None, central_dns_suffix="azureiotcentral.com", ): provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token,) - if not device_id: - return provider.get_all_registration_info( - central_dns_suffix=central_dns_suffix, device_status=device_status - ) return provider.get_device_registration_info( - device_id=device_id, - central_dns_suffix=central_dns_suffix, - device_status=device_status, + device_id=device_id, central_dns_suffix=central_dns_suffix, device_status=None, ) diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 747552403..9e5e75cee 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -9,9 +9,7 @@ """ from knack.arguments import CLIArgumentType, CaseInsensitiveList - -from azure.cli.core.commands.parameters import get_three_state_flag, get_enum_type -from azext_iot.central.models.enum import DeviceStatus +from azure.cli.core.commands.parameters import get_three_state_flag from azext_iot.monitor.models.enum import Severity from azext_iot._params import event_msg_prop_type, event_timeout_type @@ -80,12 +78,6 @@ def load_central_arguments(self, _): help="Central dns suffix. " "This enables running cli commands against non public/prod environments", ) - context.argument( - "device_status", - options_list=["--device-status", "--ds"], - arg_type=get_enum_type(DeviceStatus), - help="Indicates filter option for device status", - ) with self.argument_context("iot central app monitor-events") as context: context.argument("timeout", arg_type=event_timeout_type) diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 5af51d6ec..7a7e266aa 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -83,7 +83,10 @@ def get_device_template_by_device_id( def list_devices(self, central_dns_suffix="azureiotcentral.com") -> List[Device]: devices = central_services.device.list_devices( - 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, ) # add to cache @@ -157,6 +160,7 @@ def get_device_credentials( app_id=self._app_id, device_id=device_id, token=self._token, + central_dns_suffix=central_dns_suffix, ) if not credentials: @@ -191,7 +195,7 @@ def get_device_registration_info( dps_state = dps_global_service.get_registration_state( id_scope=id_scope, key=key, device_id=device_id ) - dps_state = self.dps_populate_essential_info(dps_state, device.device_status) + dps_state = self._dps_populate_essential_info(dps_state, device.device_status) info = { "@device_id": device_id, @@ -203,7 +207,7 @@ def get_device_registration_info( return info - def dps_populate_essential_info(self, dps_info, device_status: DeviceStatus): + def _dps_populate_essential_info(self, dps_info, device_status: DeviceStatus): error = { DeviceStatus.provisioned: "None.", DeviceStatus.registered: "Device is not yet provisioned.", @@ -217,35 +221,3 @@ def dps_populate_essential_info(self, dps_info, device_status: DeviceStatus): "error": error.get(device_status), } return filtered_dps_info - - def get_all_registration_info( - self, device_status, central_dns_suffix="azureiotcentral.com" - ): - - logger.warning("This command may take a long time to complete execution.") - devices = self.list_devices(central_dns_suffix=central_dns_suffix) - - real_devices = [device for device in devices.values() if not device.simulated] - - filtered_devices = real_devices - - if device_status: - requested_status = DeviceStatus(device_status) - filtered_devices = [ - device - for device in real_devices - if device.device_status == requested_status - ] - - if len(devices) != len(filtered_devices): - logger.warning( - "Getting registration info for real devices. " - "{}".format([device.id for device in filtered_devices]) - ) - - result = [ - self.get_device_registration_info(device.id, device.device_status) - for device in filtered_devices - ] - - return result diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 0aca61a98..8a3641bab 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -151,8 +151,11 @@ def test_central_device_template_methods_CRLD(self): map_json = map_output.get_output_in_json() assert map_json[template_name] == template_id - def test_central_device_registration_info(self): - (device_id, _) = self._create_device() + 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 + ) result = self.cmd( "iot central app device registration-info --app-id {} -d {}".format( @@ -161,8 +164,10 @@ def test_central_device_registration_info(self): ) self._delete_device(device_id) + self._delete_device_template(template_id) json_result = result.get_output_in_json() + assert json_result["@device_id"] == device_id # since time taken for provisioning to complete is not known @@ -170,58 +175,59 @@ def test_central_device_registration_info(self): assert json_result["device_registration_info"] is not None assert json_result["dps_state"] is not None - def test_central_device_registration_info_filter_unassociated(self): - device_status_expected = "unassociated" + # Validation - device registration. + device_registration_info = json_result["device_registration_info"] + assert len(device_registration_info) == 5 + 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 not device_registration_info.get("simulated") - (device_id, _) = self._create_device() + # Validation - dps state + dps_state = json_result["dps_state"] + assert len(dps_state) == 2 + assert device_registration_info.get("status") is None + assert dps_state.get("error") == "Device is not yet provisioned." + + def test_central_device_registration_info_unassociated(self): + + (device_id, device_name) = self._create_device() result = self.cmd( - "iot central app device registration-info --app-id {} --ds {}".format( - APP_ID, device_status_expected + "iot central app device registration-info --app-id {} -d {}".format( + APP_ID, device_id ) ) self._delete_device(device_id) - json_result = [] - device_info_results = [] json_result = result.get_output_in_json() - for device in json_result: - device_info_results.append(device.get("device_registration_info")) - - for device in device_info_results: - assert device.get("device_status") == device_status_expected - assert device.get("display_name") - assert device.get("id") - assert not device.get("simulated") - assert not device.get("instance_of") - - def test_central_device_registration_info_filter_registered(self): - device_status_expected = "registered" - (template_id, _) = self._create_device_template() - (device_id, _) = self._create_device(instance_of=template_id) - result = self.cmd( - "iot central app device registration-info --app-id {} --ds {}".format( - APP_ID, device_status_expected - ) - ) + assert json_result["@device_id"] == device_id - self._delete_device(device_id) - self._delete_device_template(template_id) + # since time taken for provisioning to complete is not known + # we can only assert that the payload is populated, not anything specific beyond that + assert json_result["device_registration_info"] is not None + assert json_result["dps_state"] is not None - json_result = [] - device_info_results = [] - json_result = result.get_output_in_json() - for device in json_result: - device_info_results.append(device.get("device_registration_info")) - - for device in device_info_results: - assert device.get("device_status") == device_status_expected - assert device.get("display_name") - assert device.get("id") - assert not device.get("simulated") - assert device.get("instance_of") + # Validation - device registration. + device_registration_info = json_result["device_registration_info"] + assert len(device_registration_info) == 5 + 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 not device_registration_info.get("simulated") + + # Validation - dps state + dps_state = json_result["dps_state"] + assert len(dps_state) == 2 + assert device_registration_info.get("status") is None + assert ( + dps_state.get("error") + == "Device does not have a valid template associated with it." + ) def _create_device(self, **kwargs) -> (str, str): """ From 5419c928ababf59b2f0218e792b2b2a40f44d7e1 Mon Sep 17 00:00:00 2001 From: valluriraj Date: Fri, 22 May 2020 10:39:59 -0700 Subject: [PATCH 041/179] Add support for registration summary (#192) * initial commit - add support for registration summary * address review comments * address code review comments * address code review comments * help file fixes Co-authored-by: Raj valluri --- azext_iot/central/_help.py | 19 ++++++-- azext_iot/central/command_map.py | 3 ++ azext_iot/central/commands_device.py | 9 ++++ .../central/providers/device_provider.py | 8 ++++ azext_iot/central/services/device.py | 44 ++++++++++++++++++- azext_iot/tests/test_iot_central_int.py | 15 ++++++- 6 files changed, 93 insertions(+), 5 deletions(-) diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 7ad8b422c..b6c4e27ac 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -111,7 +111,7 @@ def _load_central_devices_help(): "iot central app device registration-info" ] = """ type: command - short-summary: Get registration info for a given device in IoT Central + short-summary: Get registration info on device(s) from IoT Central long-summary: | Note: This command can take a significant amount of time to return if no device id is specified and your app contains a lot of devices @@ -119,10 +119,23 @@ def _load_central_devices_help(): examples: - name: Get registration info on specified device text: > - az iot central app device registration-info - --app-id {appid} --device-id {deviceid} + az iot central app device registration-info --app-id {appid} --device-id {deviceid} """ + helps[ + "iot central app device registration-summary" + ] = """ + type: command + short-summary: Provides a registration summary of all the devices in an app. + long-summary: | + Note: This command can take a significant amount of time to return + if your app contains a lot of devices + examples: + - name: Registration summary + text: > + az iot central app device registration-summary --app-id {appid} + """ + def _load_central_device_templates_help(): helps[ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index 53e58d373..deab7ff06 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -45,6 +45,9 @@ def load_central_commands(self, _): cmd_group.command("create", "create_device") cmd_group.command("delete", "delete_device") cmd_group.command("registration-info", "registration_info") + cmd_group.command( + "registration-summary", "registration_summary", + ) with self.command_group( "iot central app device-template", diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py index 5b80b122e..df6a36382 100644 --- a/azext_iot/central/commands_device.py +++ b/azext_iot/central/commands_device.py @@ -70,3 +70,12 @@ def registration_info( return provider.get_device_registration_info( device_id=device_id, central_dns_suffix=central_dns_suffix, device_status=None, ) + + +def registration_summary( + cmd, app_id: str, token=None, central_dns_suffix="azureiotcentral.com", +): + provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token,) + return provider.get_device_registration_summary( + central_dns_suffix=central_dns_suffix, + ) diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 7a7e266aa..d4ce0dde1 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -207,6 +207,14 @@ def get_device_registration_info( return info + def get_device_registration_summary(self, central_dns_suffix="azureiotcentral.com"): + return central_services.device.get_device_registration_summary( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + ) + def _dps_populate_essential_info(self, dps_info, device_status: DeviceStatus): error = { DeviceStatus.provisioned: "None.", diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index 20af5c216..0ea42d480 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -9,10 +9,13 @@ from knack.util import CLIError from typing import List - +from azext_iot.central.models.enum import DeviceStatus +from knack.log import get_logger from azext_iot.central.services import _utility from azext_iot.central.models.device import Device +logger = get_logger(__name__) + BASE_PATH = "api/preview/devices" @@ -84,6 +87,45 @@ def list_devices( return devices +def get_device_registration_summary( + cmd, app_id: str, token: str, central_dns_suffix="azureiotcentral.com", +): + """ + Get device registration summary for a given app + + 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 ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + registration summary + """ + + registration_summary = {status.value: 0 for status in DeviceStatus} + + url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) + headers = _utility.get_headers(token, cmd) + logger.warn( + "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) + 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 + + print("Processed {} devices...".format(sum(registration_summary.values()))) + url = result.get("nextLink") + return registration_summary + + def create_device( cmd, app_id: str, diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 8a3641bab..ec998a39d 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -8,7 +8,7 @@ import time from azure.cli.testsdk import LiveScenarioTest - +from azext_iot.central.models.enum import DeviceStatus from azext_iot.common import utility APP_ID = os.environ.get("azext_iot_central_app_id") @@ -229,6 +229,19 @@ def test_central_device_registration_info_unassociated(self): == "Device does not have a valid template associated with it." ) + def test_central_device_registration_summary(self): + + result = self.cmd( + "iot central app device registration-summary --app-id {}".format(APP_ID,) + ) + + json_result = result.get_output_in_json() + assert json_result[DeviceStatus.provisioned.value] is not None + assert json_result[DeviceStatus.registered.value] is not None + assert json_result[DeviceStatus.unassociated.value] is not None + assert json_result[DeviceStatus.blocked.value] is not None + assert len(json_result) == 4 + def _create_device(self, **kwargs) -> (str, str): """ kwargs: From ff8e33a7e3da9c9a5e391d2c9541a69676d45fe0 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 26 May 2020 15:42:26 -0700 Subject: [PATCH 042/179] Update CODEOWNERS --- .github/CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d4870c31d..68f7f0398 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,11 +8,11 @@ azext_iot/central/ @prbans # Monitor Code Owner(s) -azext_iot/monitor/ @prbans +azext_iot/monitor/ @prbans @digimaun # Tests Code Owners azext_iot/tests/central/ @prbans azext_iot/tests/test_iot_central_int.py @prbans azext_iot/tests/test_iot_central_unit.py @prbans -azext_iot/tests/test_monitor_parsers_unit.py @prbans -azext_iot/tests/test_uamqp_import.py @prbans +azext_iot/tests/test_monitor_parsers_unit.py @prbans @digimaun +azext_iot/tests/test_uamqp_import.py @prbans @digimaun From 00512fcfa9096a39548476dba1b3294b7765cea0 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Mon, 1 Jun 2020 10:36:02 -0700 Subject: [PATCH 043/179] Tweak and HISTORY.rst updates. --- HISTORY.rst | 28 +++++++++++++++++++++++++++- dev_requirements | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 030110325..103efd8a5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,11 +3,37 @@ Release History =============== +0.9.3 ++++++++++++++++ + * IoT Hub device identity import/export commands support usage via managed service identity using the --auth-type argument. + * Adds preview command group "az iot central app device" + * Adds preview command "az iot central app device create" + * Adds preview command "az iot central app device show" + * Adds preview command "az iot central app device list" + * Adds preview command "az iot central app device delete" + * Adds preview command "az iot central app device registration-info" + * Adds preview command "az iot central app device registration-summary" + * Adds preview command group "az iot central app device-template" + * Adds preview command "az iot central app device-template create" + * Adds preview command "az iot central app device-template show" + * Adds preview command "az iot central app device-template list" + * Adds preview command "az iot central app device-template delete" + * Adds preview command "az iot central app device-template map" + * Changed how results are displayed in "az iot central app validate-messages" + +* Known issues: + * The following preview commands will retrieve at most 25 results + * az iot central app device list + * az iot central app device-template list + * az iot central app device-template map + + 0.9.2 +++++++++++++++ * Device and module twin update operations provide explicit patch arguments (--desired, --tags). * Adds command "az iot central app validate-messages" -* Remove Py 2.7 remnants from setup manifest. +* Remove Py 2.7 support and remnants from setup manifest. +* Remove Py 3.4 support and remnants from setup manifest. 0.9.1 +++++++++++++++ diff --git a/dev_requirements b/dev_requirements index dcd5556c4..d6e877036 100644 --- a/dev_requirements +++ b/dev_requirements @@ -10,4 +10,4 @@ flake8 black;python_version>='3.6' wheel==0.30.0 pre-commit -six==1.12 +six>=1.12 From 0e1fa806ba2df4b3ce26ad82786a5feec7596ea7 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 4 Jun 2020 16:47:10 -0700 Subject: [PATCH 044/179] Azure Digital Twins CLI Public Preview baseline (#195) Introducing 35 new commands in the following command groups - az dt - az dt endpoint - az dt model - az dt role-assignment - az dt route - az dt twin - az dt twin relationship - az dt twin telemety --- azext_iot/__init__.py | 4 + azext_iot/central/params.py | 2 +- azext_iot/common/embedded_cli.py | 53 + azext_iot/common/utility.py | 16 +- azext_iot/constants.py | 4 +- azext_iot/digitaltwins/__init__.py | 9 + azext_iot/digitaltwins/_help.py | 592 ++++++++++ azext_iot/digitaltwins/command_map.py | 119 ++ azext_iot/digitaltwins/commands_models.py | 46 + azext_iot/digitaltwins/commands_rbac.py | 39 + azext_iot/digitaltwins/commands_resource.py | 146 +++ azext_iot/digitaltwins/commands_routes.py | 30 + azext_iot/digitaltwins/commands_twins.py | 132 +++ azext_iot/digitaltwins/common.py | 40 + azext_iot/digitaltwins/params.py | 289 +++++ azext_iot/digitaltwins/providers/__init__.py | 42 + azext_iot/digitaltwins/providers/auth.py | 59 + azext_iot/digitaltwins/providers/base.py | 37 + azext_iot/digitaltwins/providers/generic.py | 43 + azext_iot/digitaltwins/providers/model.py | 110 ++ azext_iot/digitaltwins/providers/rbac.py | 56 + azext_iot/digitaltwins/providers/resource.py | 333 ++++++ azext_iot/digitaltwins/providers/route.py | 58 + azext_iot/digitaltwins/providers/twin.py | 251 ++++ azext_iot/sdk/digitaltwins/__init__.py | 18 + .../digitaltwins/azure_digital_twins_api.py | 90 ++ azext_iot/sdk/digitaltwins/models/__init__.py | 54 + .../digital_twin_models_list_options.py | 30 + .../digital_twin_models_list_options_py3.py | 30 + azext_iot/sdk/digitaltwins/models/error.py | 51 + .../sdk/digitaltwins/models/error_py3.py | 51 + .../sdk/digitaltwins/models/error_response.py | 41 + .../digitaltwins/models/error_response_py3.py | 41 + .../sdk/digitaltwins/models/event_route.py | 51 + .../digitaltwins/models/event_route_paged.py | 27 + .../digitaltwins/models/event_route_py3.py | 51 + .../models/event_routes_list_options.py | 30 + .../models/event_routes_list_options_py3.py | 30 + .../models/incoming_relationship.py | 43 + .../models/incoming_relationship_paged.py | 27 + .../models/incoming_relationship_py3.py | 43 + .../sdk/digitaltwins/models/inner_error.py | 35 + .../digitaltwins/models/inner_error_py3.py | 35 + .../sdk/digitaltwins/models/model_data.py | 59 + .../digitaltwins/models/model_data_paged.py | 28 + .../sdk/digitaltwins/models/model_data_py3.py | 59 + .../sdk/digitaltwins/models/object_paged.py | 27 + .../sdk/digitaltwins/models/query_result.py | 33 + .../digitaltwins/models/query_result_py3.py | 33 + .../models/query_specification.py | 35 + .../models/query_specification_py3.py | 35 + .../sdk/digitaltwins/operations/__init__.py | 22 + .../digital_twin_models_operations.py | 363 ++++++ .../operations/digital_twins_operations.py | 1037 +++++++++++++++++ .../operations/event_routes_operations.py | 292 +++++ .../operations/query_operations.py | 109 ++ azext_iot/sdk/digitaltwins/version.py | 13 + azext_iot/sdk/digitaltwins_arm/__init__.py | 18 + .../azure_digital_twins_management_client.py | 99 ++ .../sdk/digitaltwins_arm/models/__init__.py | 93 ++ ...e_digital_twins_management_client_enums.py | 45 + .../models/check_name_request.py | 45 + .../models/check_name_request_py3.py | 45 + .../models/check_name_result.py | 43 + .../models/check_name_result_py3.py | 43 + .../models/digital_twins_description.py | 74 ++ .../models/digital_twins_description_paged.py | 27 + .../models/digital_twins_description_py3.py | 74 ++ .../models/digital_twins_endpoint_resource.py | 59 + .../digital_twins_endpoint_resource_paged.py | 28 + ...ital_twins_endpoint_resource_properties.py | 61 + ..._twins_endpoint_resource_properties_py3.py | 61 + .../digital_twins_endpoint_resource_py3.py | 59 + .../models/digital_twins_patch_description.py | 28 + .../digital_twins_patch_description_py3.py | 28 + .../models/digital_twins_resource.py | 57 + .../models/digital_twins_resource_py3.py | 56 + .../models/digital_twins_sku_info.py | 35 + .../models/digital_twins_sku_info_py3.py | 35 + .../models/error_definition.py | 46 + .../models/error_definition_py3.py | 46 + .../digitaltwins_arm/models/error_response.py | 41 + .../models/error_response_py3.py | 41 + .../sdk/digitaltwins_arm/models/event_grid.py | 67 ++ .../digitaltwins_arm/models/event_grid_py3.py | 67 ++ .../sdk/digitaltwins_arm/models/event_hub.py | 63 + .../digitaltwins_arm/models/event_hub_py3.py | 63 + .../models/external_resource.py | 45 + .../models/external_resource_py3.py | 45 + .../models/integration_resource.py | 61 + .../models/integration_resource_paged.py | 27 + .../models/integration_resource_py3.py | 61 + .../models/integration_resource_state1.py | 38 + .../models/integration_resource_state1_py3.py | 38 + .../integration_resource_update_info.py | 50 + .../integration_resource_update_info_py3.py | 50 + .../sdk/digitaltwins_arm/models/operation.py | 40 + .../models/operation_display.py | 50 + .../models/operation_display_py3.py | 50 + .../models/operation_paged.py | 27 + .../digitaltwins_arm/models/operation_py3.py | 40 + .../digitaltwins_arm/models/service_bus.py | 63 + .../models/service_bus_py3.py | 63 + .../digitaltwins_arm/operations/__init__.py | 20 + .../digital_twins_endpoint_operations.py | 382 ++++++ .../operations/digital_twins_operations.py | 602 ++++++++++ .../digitaltwins_arm/operations/operations.py | 96 ++ azext_iot/sdk/digitaltwins_arm/version.py | 12 + azext_iot/tests/digitaltwins/__init__.py | 78 ++ azext_iot/tests/digitaltwins/conftest.py | 14 + .../tests/digitaltwins/models/Floor.json | 22 + .../digitaltwins/models/nested/Room.json | 22 + .../models/nested/deeper/Thermostat.json | 18 + .../test_dt_model_lifecycle_int.py | 215 ++++ .../test_dt_resource_lifecycle_int.py | 537 +++++++++ .../test_dt_twin_lifecycle_int.py | 410 +++++++ azext_iot/tests/settings.py | 10 +- dev_requirements | 2 +- pytest.ini.example | 10 + 119 files changed, 10169 insertions(+), 9 deletions(-) create mode 100644 azext_iot/common/embedded_cli.py create mode 100644 azext_iot/digitaltwins/__init__.py create mode 100644 azext_iot/digitaltwins/_help.py create mode 100644 azext_iot/digitaltwins/command_map.py create mode 100644 azext_iot/digitaltwins/commands_models.py create mode 100644 azext_iot/digitaltwins/commands_rbac.py create mode 100644 azext_iot/digitaltwins/commands_resource.py create mode 100644 azext_iot/digitaltwins/commands_routes.py create mode 100644 azext_iot/digitaltwins/commands_twins.py create mode 100644 azext_iot/digitaltwins/common.py create mode 100644 azext_iot/digitaltwins/params.py create mode 100644 azext_iot/digitaltwins/providers/__init__.py create mode 100644 azext_iot/digitaltwins/providers/auth.py create mode 100644 azext_iot/digitaltwins/providers/base.py create mode 100644 azext_iot/digitaltwins/providers/generic.py create mode 100644 azext_iot/digitaltwins/providers/model.py create mode 100644 azext_iot/digitaltwins/providers/rbac.py create mode 100644 azext_iot/digitaltwins/providers/resource.py create mode 100644 azext_iot/digitaltwins/providers/route.py create mode 100644 azext_iot/digitaltwins/providers/twin.py create mode 100644 azext_iot/sdk/digitaltwins/__init__.py create mode 100644 azext_iot/sdk/digitaltwins/azure_digital_twins_api.py create mode 100644 azext_iot/sdk/digitaltwins/models/__init__.py create mode 100644 azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options.py create mode 100644 azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/models/error.py create mode 100644 azext_iot/sdk/digitaltwins/models/error_py3.py create mode 100644 azext_iot/sdk/digitaltwins/models/error_response.py create mode 100644 azext_iot/sdk/digitaltwins/models/error_response_py3.py create mode 100644 azext_iot/sdk/digitaltwins/models/event_route.py create mode 100644 azext_iot/sdk/digitaltwins/models/event_route_paged.py create mode 100644 azext_iot/sdk/digitaltwins/models/event_route_py3.py create mode 100644 azext_iot/sdk/digitaltwins/models/event_routes_list_options.py create mode 100644 azext_iot/sdk/digitaltwins/models/event_routes_list_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/models/incoming_relationship.py create mode 100644 azext_iot/sdk/digitaltwins/models/incoming_relationship_paged.py create mode 100644 azext_iot/sdk/digitaltwins/models/incoming_relationship_py3.py create mode 100644 azext_iot/sdk/digitaltwins/models/inner_error.py create mode 100644 azext_iot/sdk/digitaltwins/models/inner_error_py3.py create mode 100644 azext_iot/sdk/digitaltwins/models/model_data.py create mode 100644 azext_iot/sdk/digitaltwins/models/model_data_paged.py create mode 100644 azext_iot/sdk/digitaltwins/models/model_data_py3.py create mode 100644 azext_iot/sdk/digitaltwins/models/object_paged.py create mode 100644 azext_iot/sdk/digitaltwins/models/query_result.py create mode 100644 azext_iot/sdk/digitaltwins/models/query_result_py3.py create mode 100644 azext_iot/sdk/digitaltwins/models/query_specification.py create mode 100644 azext_iot/sdk/digitaltwins/models/query_specification_py3.py create mode 100644 azext_iot/sdk/digitaltwins/operations/__init__.py create mode 100644 azext_iot/sdk/digitaltwins/operations/digital_twin_models_operations.py create mode 100644 azext_iot/sdk/digitaltwins/operations/digital_twins_operations.py create mode 100644 azext_iot/sdk/digitaltwins/operations/event_routes_operations.py create mode 100644 azext_iot/sdk/digitaltwins/operations/query_operations.py create mode 100644 azext_iot/sdk/digitaltwins/version.py create mode 100644 azext_iot/sdk/digitaltwins_arm/__init__.py create mode 100644 azext_iot/sdk/digitaltwins_arm/azure_digital_twins_management_client.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/__init__.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/azure_digital_twins_management_client_enums.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/check_name_request.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/check_name_request_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/check_name_result.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/check_name_result_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_description.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_paged.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_paged.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/error_definition.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/error_definition_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/error_response.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/error_response_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/event_grid.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/event_grid_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/event_hub.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/event_hub_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/external_resource.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/external_resource_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource_paged.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/operation.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/operation_display.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/operation_display_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/operation_paged.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/operation_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/service_bus.py create mode 100644 azext_iot/sdk/digitaltwins_arm/models/service_bus_py3.py create mode 100644 azext_iot/sdk/digitaltwins_arm/operations/__init__.py create mode 100644 azext_iot/sdk/digitaltwins_arm/operations/digital_twins_endpoint_operations.py create mode 100644 azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py create mode 100644 azext_iot/sdk/digitaltwins_arm/operations/operations.py create mode 100644 azext_iot/sdk/digitaltwins_arm/version.py create mode 100644 azext_iot/tests/digitaltwins/__init__.py create mode 100644 azext_iot/tests/digitaltwins/conftest.py create mode 100644 azext_iot/tests/digitaltwins/models/Floor.json create mode 100644 azext_iot/tests/digitaltwins/models/nested/Room.json create mode 100644 azext_iot/tests/digitaltwins/models/nested/deeper/Thermostat.json create mode 100644 azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py create mode 100644 azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py create mode 100644 azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py diff --git a/azext_iot/__init__.py b/azext_iot/__init__.py index 25f7d809e..ff0b70d6f 100644 --- a/azext_iot/__init__.py +++ b/azext_iot/__init__.py @@ -39,19 +39,23 @@ def load_command_table(self, args): from azext_iot.commands import load_command_table from azext_iot.iothub.command_bindings import load_iothub_commands from azext_iot.central.command_map import load_central_commands + from azext_iot.digitaltwins.command_map import load_digitaltwins_commands load_command_table(self, args) load_iothub_commands(self, args) load_central_commands(self, args) + load_digitaltwins_commands(self, args) return self.command_table def load_arguments(self, command): from azext_iot._params import load_arguments from azext_iot.central.params import load_central_arguments + from azext_iot.digitaltwins.params import load_digitaltwins_arguments load_arguments(self, command) load_central_arguments(self, command) + load_digitaltwins_arguments(self, command) COMMAND_LOADER_CLS = IoTExtCommandsLoader diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 9e5e75cee..647424354 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -1,7 +1,7 @@ # coding=utf-8 # -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. -# Unpublished works. +# Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- """ diff --git a/azext_iot/common/embedded_cli.py b/azext_iot/common/embedded_cli.py new file mode 100644 index 000000000..cbe96b926 --- /dev/null +++ b/azext_iot/common/embedded_cli.py @@ -0,0 +1,53 @@ +# 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 shlex +from azure.cli.core import get_default_cli +from knack.log import get_logger +from knack.util import CLIError +from io import StringIO + +logger = get_logger(__name__) + + +class EmbeddedCLI(object): + def __init__(self): + super(EmbeddedCLI, self).__init__() + self.output = "" + self.error_code = 0 + self.az_cli = get_default_cli() + + def invoke(self, command): + output_file = StringIO() + command = self._ensure_json_output(command) + self.error_code = self.az_cli.invoke(shlex.split(command), out_file=output_file) or 0 + self.output = output_file.getvalue() + logger.debug( + "Embedded CLI received error code: %s, output: '%s'", + self.error_code, + self.output, + ) + output_file.close() + + return self + + def as_json(self): + try: + return json.loads(self.output) + except: + raise CLIError( + "Issue parsing received payload '{}' as json. Please try again or check resource status.".format( + self.output + ) + ) + + def success(self): + logger.debug("Operation error code: %s", self.error_code) + return self.error_code == 0 + + def _ensure_json_output(self, command): + return "{} -o json".format(command) diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index 92dc6b727..a3c3b114a 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -169,15 +169,15 @@ def read_file_content(file_path, allow_binary=False): for encoding in ["utf-8-sig", "utf-8", "utf-16", "utf-16le", "utf-16be"]: try: with codecs_open(file_path, encoding=encoding) as f: - logger.debug("attempting to read file %s as %s", file_path, encoding) + logger.debug("Attempting to read file %s as %s", file_path, encoding) return f.read() except (UnicodeError, UnicodeDecodeError): pass if allow_binary: try: - with open(file_path, "rb") as input_file: - logger.debug("attempting to read file %s as binary", file_path) + with open(file_path, 'rb') as input_file: + logger.debug("Attempting to read file %s as binary", file_path) return base64.b64encode(input_file.read()).decode("utf-8") except Exception: # pylint: disable=broad-except pass @@ -467,3 +467,13 @@ def is_iso8601_time(self, to_validate: str) -> bool: def ensure_min_version(cur_ver, min_ver): from pkg_resources._vendor.packaging import version return version.parse(cur_ver) >= version.parse(min_ver) + + +def scantree(path): + from os import scandir + + for entry in scandir(path): + if entry.is_dir(follow_symlinks=False): + yield from scantree(entry.path) + else: + yield entry diff --git a/azext_iot/constants.py b/azext_iot/constants.py index be276320f..c549b6bce 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.3" +VERSION = "0.9.4" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" @@ -42,7 +42,7 @@ TRACING_ALLOWED_FOR_LOCATION = ("northeurope", "westus2", "west us 2", "southeastasia") TRACING_ALLOWED_FOR_SKU = "standard" USER_AGENT = "IoTPlatformCliExtension/{}".format(VERSION) - +DIGITALTWINS_RESOURCE_ID = "https://digitaltwins.azure.net" # (Lib name, minimum version (including), maximum version (excluding)) EVENT_LIB = ("uamqp", "1.2", "1.3") diff --git a/azext_iot/digitaltwins/__init__.py b/azext_iot/digitaltwins/__init__.py new file mode 100644 index 000000000..77cf20823 --- /dev/null +++ b/azext_iot/digitaltwins/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from ._help import load_digitaltwins_help + +load_digitaltwins_help() diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py new file mode 100644 index 000000000..139f5bb6f --- /dev/null +++ b/azext_iot/digitaltwins/_help.py @@ -0,0 +1,592 @@ +# 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. +# -------------------------------------------------------------------------------------------- +""" +Help definitions for IoT Hub commands. +""" + +from knack.help_files import helps + + +def load_digitaltwins_help(): + + helps["dt"] = """ + type: group + short-summary: Manage Azure Digital Twins (ADT) solutions & infrastructure. + """ + + helps["dt create"] = """ + type: command + short-summary: Create a new Digital Twins instance. + + examples: + - name: Create instance in target resource group with default location. + text: > + az dt create -n {instance_name} -g {resouce_group} -l eastus2euap + + - name: Create instance in target resource group with specified location and tags. + text: > + az dt create -n {instance_name} -g {resouce_group} -l westcentralus --tags "a=b;c=d" + """ + + helps["dt show"] = """ + type: command + short-summary: Show an existing Digital Twins instance. + + examples: + - name: Show an instance. + text: > + az dt show -n {instance_name} + + - name: Show an instance and project certain properties. + text: > + az dt show -n {instance_name} --query "{Endpoint:hostName, Location:location}" + """ + + helps["dt list"] = """ + type: command + short-summary: List the collection of Digital Twins instances by subscription or resource group. + + examples: + - name: List all instances in the current subscription. + text: > + az dt list + + - name: List all instances in target resource group and output in table format. + text: > + az dt list -g {resource_group} --output table + + - name: List all instances in subscription that meet a condition. + text: > + az dt list --query "[?contains(name, 'Production')]" + + - name: Count instances that meet condition. + text: > + az dt list --query "length([?contains(name, 'Production')])" + """ + + helps["dt delete"] = """ + type: command + short-summary: Delete an existing Digital Twins instance. + + examples: + - name: Delete an arbitrary instance. + text: > + az dt delete -n {instance_name} + """ + + helps["dt endpoint"] = """ + type: group + short-summary: Manage and configure Digital Twins instance endpoints. + """ + + helps["dt endpoint create"] = """ + type: group + short-summary: Add egress endpoints to a Digital Twins instance. + """ + + helps["dt endpoint create eventgrid"] = """ + type: command + short-summary: Adds an EventGrid Topic endpoint to a Digital Twins instance. + Requires pre-created resource. + + examples: + - name: Adds an EventGrid Topic endpoint to a target instance. + text: > + az dt endpoint create eventgrid --endpoint-name {endpoint_name} + --eventgrid-resource-group {eventgrid_resource_group} + --eventgrid-topic {eventgrid_topic_name} + -n {instance_name} + """ + + helps["dt endpoint create eventhub"] = """ + type: command + short-summary: Adds an EventHub endpoint to a Digital Twins instance. + Requires pre-created resource. + + examples: + - name: Adds an EventHub endpoint to a target instance. + text: > + az dt endpoint create eventhub --endpoint-name {endpoint_name} + --eventhub-resource-group {eventhub_resource_group} + --eventhub-namespace {eventhub_namespace} + --eventhub {eventhub_name} + --eventhub-policy {eventhub_policy} + -n {instance_name} + """ + + helps["dt endpoint create servicebus"] = """ + type: command + short-summary: Adds a ServiceBus Topic endpoint to a Digital Twins instance. + Requires pre-created resource. + + examples: + - name: Adds a ServiceBus Topic endpoint to a target instance. + text: > + az dt endpoint create servicebus --endpoint-name {endpoint_name} + --servicebus-resource-group {servicebus_resource_group} + --servicebus-namespace {servicebus_namespace} + --servicebus-topic {servicebus_topic_name} + --servicebus-policy {servicebus_policy} + -n {instance_name} + """ + + helps["dt endpoint list"] = """ + type: command + short-summary: List all egress endpoints configured on a Digital Twins instance. + + examples: + - name: List all egress endpoints configured on an instance. + text: > + az dt endpoint list -n {instance_name} + """ + + helps["dt endpoint show"] = """ + type: command + short-summary: Show details of an endpoint configured on a Digital Twins instance. + + examples: + - name: Show a desired endpoint by name on an instance. + text: > + az dt endpoint show -n {instance_name} --endpoint-name {endpoint_name} + """ + + helps["dt endpoint delete"] = """ + type: command + short-summary: Remove an endpoint from a Digital Twins instance. + + examples: + - name: Remove an endpoint from an instance. + text: > + az dt endpoint delete -n {instance_name} --endpoint-name {endpoint_name} + """ + + helps["dt role-assignment"] = """ + type: group + short-summary: Manage RBAC role assignments for a Digital Twins instance. + long-summary: | + Note that in order to perform role assignments, the logged in principal needs permissions + such as Owner or User Access Administrator at the assigned scope. + + This command group is provided for convenience. For more complex role assignment scenarios + use the 'az role assignment' command group. + """ + + helps["dt role-assignment create"] = """ + type: command + short-summary: Assign a user, group or service principal to a role against a Digital Twins instance. + long-summary: + Note that in order to perform role assignments, the logged in principal needs permissions + such as Owner or User Access Administrator at the assigned scope. + + examples: + - name: Assign a user the built-in Digital Twins Owner role against a target instance. + text: > + az dt role-assignment create -n {instance_name} --assignee "owneruser@microsoft.com" --role "Azure Digital Twins Owner (Preview)" + + - name: Assign a user the built-in Digital Twins Reader role against a target instance. + text: > + az dt role-assignment create -n {instance_name} --assignee "readeruser@microsoft.com" --role "Azure Digital Twins Reader (Preview)" + + - name: Assign a service principal a custom role against a target instance. + text: > + az dt role-assignment create -n {instance_name} --assignee {service_principal_name_or_id} --role {role_name_or_id} + """ + + helps["dt role-assignment delete"] = """ + type: command + short-summary: Remove a user, group or service principal role assignment from a Digital Twins instance. + long-summary: + Note that in order to perform role assignments, the logged in principal needs permissions + such as Owner or User Access Administrator at the assigned scope. + + examples: + - name: Remove a user from a specific role assignment of a Digital Twins instance. + text: > + az dt role-assignment delete -n {instance_name} --assignee "removeuser@microsoft.com" --role "Azure Digital Twins Reader (Preview)" + + - name: Remove a user from all assigned roles of a Digital Twins instance. + text: > + az dt role-assignment delete -n {instance_name} --assignee "removeuser@microsoft.com" + """ + + helps["dt role-assignment list"] = """ + type: command + short-summary: List the existing role assignments of a Digital Twins instance. + + examples: + - name: List the role assignments on a target instance. + text: > + az dt role-assignment list -n {instance_name} + + - name: List the role assignments on a target instance and filter by role. + text: > + az dt role-assignment list -n {instance_name} --role {role_name_or_id} + """ + + helps["dt route"] = """ + type: group + short-summary: Manage and configure event routes. + long-summary: + Note that an endpoint must first be configred before adding an event route. + """ + + helps["dt route create"] = """ + type: command + short-summary: Add an event route to a Digital Twins instance. + + examples: + - name: Adds an event route for an existing endpoint on target instance with default filter of "true". + text: > + az dt route create -n {instance_name} --endpoint-name {endpoint_name} --route-name {route_name} + - name: Adds an event route for an existing endpoint on target instance with custom filter. + text: > + az dt route create -n {instance_name} --endpoint-name {endpoint_name} --route-name {route_name} + --filter "type = 'Microsoft.DigitalTwins.Twin.Create'" + """ + + helps["dt route list"] = """ + type: command + short-summary: List the configured event routes of a Digital Twins instance. + + examples: + - name: List configured event routes of a target instance. + text: > + az dt route list -n {instance_name} + """ + + helps["dt route delete"] = """ + type: command + short-summary: Remove an event route from a Digital Twins instance. + + examples: + - name: Remove an event route from a target instance. + text: > + az dt route delete -n {instance_name} --route-name {route_name} + """ + + helps["dt route show"] = """ + type: command + short-summary: Show details of an event route configured on a Digital Twins instance. + + examples: + - name: Show an event route on a target instance. + text: > + az dt route show -n {instance_name} --route-name {route_name} + """ + + helps["dt twin"] = """ + type: group + short-summary: Manage and configure the digital twins of a Digital Twins instance. + """ + + helps["dt twin create"] = """ + type: command + short-summary: Create a digital twin on an instance. + long-summary: --properties can be inline JSON or file path. + + examples: + - name: Create a digital twin from an existing (prior-created) model. + text: > + az dt twin create -n {instance_name} --dtmi urn:azureiot:DeviceManagement:DeviceInformation:1 + --twin-id {twin_id} + + - name: Create a digital twin from an existing (prior-created) model. Instantiate with property values. + text: > + az dt twin create -n {instance_name} --dtmi urn:azureiot:DeviceManagement:DeviceInformation:1 + --twin-id {twin_id} --properties '{"manufacturer": "Microsoft"}' + + - name: Create a digital twin with component from existing (prior-created) models. Instantiate with property values. + text: > + az dt twin create -n {instance_name} --dtmi dtmi:example:Room;1 --twin-id {twin_id} --properties '{ + "Temperature": 10.2, + "Thermostat": { + "$metadata": { + "$model": "dtmi:com:example:Thermostat;1" + }, + "setPointTemp": 23.12 + } + }' + """ + + helps["dt twin update"] = """ + type: command + short-summary: Update an instance digital twin via JSON patch specification. + long-summary: Updates to property values and $model elements may happen + in the same request. Operations are limited to add, replace and remove. + + examples: + - name: Update a digital twin via JSON patch specification. + text: > + az dt twin update -n {instance_name} --twin-id {twin_id} + --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' + + - name: Update a digital twin via JSON patch specification. + text: > + az dt twin update -n {instance_name} --twin-id {twin_id} + --json-patch '[ + {"op":"replace", "path":"/Temperature", "value": 20.5}, + {"op":"add", "path":"/Areas", "value": ["ControlSystem"]} + ]' + + - name: Update a digital twin via JSON patch specification defined in a file. + text: > + az dt twin update -n {instance_name} --twin-id {twin_id} + --json-patch ./my/patch/document.json + """ + + helps["dt twin show"] = """ + type: command + short-summary: Show the details of a digital twin. + + examples: + - name: Show the details of a digital twin. + text: > + az dt twin show -n {instance_name} --twin-id {twin_id} + """ + + helps["dt twin query"] = """ + type: command + short-summary: Query the digital twins of an instance. Allows traversing relationships and filtering by property values. + + examples: + - name: Query all digital twins in target instance and project all attributes. Also show cost in query units. + text: > + az dt twin query -n {instance_name} -q "select * from digitaltwins" --show-cost + + - name: Query by model and project all attributes. + text: > + az dt twin query -n {instance_name} -q "select * from digitaltwins T where IS_OF_MODEL(T, 'dtmi:example:Room;2')" + """ + + helps["dt twin delete"] = """ + type: command + short-summary: Remove a digital twin. All relationships referencing this twin must already be deleted. + + examples: + - name: Remove a digital twin by Id. + text: > + az dt twin delete -n {instance_name} --twin-id {twin_id} + """ + + helps["dt twin relationship"] = """ + type: group + short-summary: Manage and configure the digital twin relationships of a Digital Twins instance. + """ + + helps["dt twin relationship create"] = """ + type: command + short-summary: Create a relationship between source and target digital twins. + long-summary: --properties can be inline JSON or file path. + + examples: + - name: Create a relationship between two digital twins. + text: > + az dt twin relationship create -n {instance_name} --relationship-id {relationship_id} --relationship contains + --source {source_twin_id} --target {target_twin_id} + + - name: Create a relationship with initialized properties between two digital twins. + text: > + az dt twin relationship create -n {instance_name} --relationship-id {relationship_id} --relationship contains + --source {source_twin_id} --target {target_twin_id} + --properties '{"ownershipUser": "me", "ownershipDepartment": "Computer Science"}' + """ + + helps["dt twin relationship show"] = """ + type: command + short-summary: Show details of a digital twin relationship. + + examples: + - name: Show details of a digital twin relationship. + text: > + az dt twin relationship show -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + """ + + helps["dt twin relationship list"] = """ + type: command + short-summary: List the relationships of a digital twin. + + examples: + - name: List outgoing relationships of a digital twin. + text: > + az dt twin relationship list -n {instance_name} --twin-id {twin_id} + + - name: List outgoing relationships of a digital twin and filter on relationship 'contains' + text: > + az dt twin relationship list -n {instance_name} --twin-id {twin_id} --relationship contains + + - name: List incoming relationships of a digital twin. + text: > + az dt twin relationship list -n {instance_name} --twin-id {twin_id} --incoming + + - name: List incoming relationships of a digital twin and filter on relationship 'contains'. + text: > + az dt twin relationship list -n {instance_name} --twin-id {twin_id} --relationship contains --incoming + """ + + helps["dt twin relationship update"] = """ + type: command + short-summary: Updates the properties of a relationship between two + digital twins via JSON patch specification. + long-summary: Operations are limited to add, replace and remove. + + examples: + - name: Update a digital twin relationship via JSON patch specification. + text: > + az dt twin relationship update -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + --relationship contains --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' + + - name: Update a digital twin relationship via JSON patch specification. + text: > + az dt twin relationship update -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + --relationship contains --json-patch '[ + {"op":"replace", "path":"/Temperature", "value": 20.5}, + {"op":"add", "path":"/Areas", "value": ["ControlSystem"]} + ]' + + - name: Update a digital twin relationship via JSON patch specification defined in a file. + text: > + az dt twin relationship update -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + --relationship contains --json-patch ./my/patch/document.json + """ + + helps["dt twin relationship delete"] = """ + type: command + short-summary: Delete a digital twin relationship on a Digital Twins instance. + + examples: + - name: Delete a digital twin relationship. + text: > + az dt twin relationship delete -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + """ + + helps["dt twin telemetry"] = """ + type: group + short-summary: Test and validate the event routes and endpoints of a Digital Twins instance. + """ + + helps["dt twin telemetry send"] = """ + type: command + short-summary: Sends telemetry on behalf of a digital twin. If component path is provided the + emitted telemetry is on behalf of the component. + + examples: + - name: Send twin telemetry + text: > + az dt twin telemetry send -n {instance_name} --twin-id {twin_id} + """ + + helps["dt model"] = """ + type: group + short-summary: Manage DTDL models and definitions on a Digital Twins instance. + """ + + helps["dt twin component"] = """ + type: group + short-summary: Show and update the digital twin components of a Digital Twins instance. + """ + + helps["dt twin component show"] = """ + type: command + short-summary: Show details of a digital twin component. + + examples: + - name: Show details of a digital twin component + text: > + az dt twin component show -n {instance_name} --twin-id {twin_id} --component Thermostat + """ + + helps["dt twin component update"] = """ + type: command + short-summary: Update a digital twin component via JSON patch specification. + long-summary: Updates to property values and $model elements may happen + in the same request. Operations are limited to add, replace and remove. + + examples: + - name: Update a digital twin component via JSON patch specification. + text: > + az dt twin component update -n {instance_name} --twin-id {twin_id} --component {component_path} + --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' + + - name: Update a digital twin component via JSON patch specification. + text: > + az dt twin component update -n {instance_name} --twin-id {twin_id} --component {component_path} + --json-patch '[ + {"op":"replace", "path":"/Temperature", "value": 20.5}, + {"op":"add", "path":"/Areas", "value": ["ControlSystem"]} + ]' + + - name: Update a digital twin component via JSON patch specification defined in a file. + text: > + az dt twin component update -n {instance_name} --twin-id {twin_id} --component {component_path} + --json-patch ./my/patch/document.json + """ + + helps["dt model create"] = """ + type: command + short-summary: Uploads one or more models. When any error occurs, no models are uploaded. + long-summary: --models can be inline json or file path. + + examples: + - name: Bulk upload all .json or .dtdl model files from a target directory. Model processing is recursive. + text: > + az dt model create -n {instance_name} --from-directory {directory_path} + + - name: Upload model json inline or from file path. + text: > + az dt model create -n {instance_name} --models {file_path_or_inline_json} + """ + + helps["dt model show"] = """ + type: command + short-summary: Retrieve a target model or model definition. + + examples: + - name: Show model meta data + text: > + az dt model show -n {instance_name} --dtmi "dtmi:example:Floor;1" + + - name: Show model meta data and definition + text: > + az dt model show -n {instance_name} --dtmi "dtmi:example:Floor;1" --definition + """ + + helps["dt model list"] = """ + type: command + short-summary: List model metadata, definitions and dependencies. + + examples: + - name: List model metadata + text: > + az dt model list -n {instance_name} + + - name: List model definitions + text: > + az dt model list -n {instance_name} --definition + + - name: List dependencies of particular pre-existing model(s). Space seperate dtmi values. + text: > + az dt model list -n {instance_name} --dependencies-for {model_id0} {model_id1} + """ + + helps["dt model update"] = """ + type: command + short-summary: Updates the metadata for a model. Currently a model can only be decommisioned. + + examples: + - name: Decommision a target model + text: > + az dt model update -n {instance_name} --dtmi "dtmi:example:Floor;1" --decommission + """ + + helps["dt model delete"] = """ + type: command + short-summary: Delete a model. A model can only be deleted if no other models reference it. + + examples: + - name: Delete a target model. + text: > + az dt model delete -n {instance_name} --dtmi "dtmi:example:Floor;1" + """ diff --git a/azext_iot/digitaltwins/command_map.py b/azext_iot/digitaltwins/command_map.py new file mode 100644 index 000000000..85c04c8bf --- /dev/null +++ b/azext_iot/digitaltwins/command_map.py @@ -0,0 +1,119 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.profiles import ResourceType + +""" +Load CLI commands +""" +from azure.cli.core.commands import CliCommandType + +digitaltwins_resource_ops = CliCommandType( + operations_tmpl="azext_iot.digitaltwins.commands_resource#{}" +) + +digitaltwins_route_ops = CliCommandType( + operations_tmpl="azext_iot.digitaltwins.commands_routes#{}" +) + +digitaltwins_model_ops = CliCommandType( + operations_tmpl="azext_iot.digitaltwins.commands_models#{}" +) + +digitaltwins_twin_ops = CliCommandType( + operations_tmpl="azext_iot.digitaltwins.commands_twins#{}" +) + +digitaltwins_rbac_ops = CliCommandType( + operations_tmpl="azext_iot.digitaltwins.commands_rbac#{}" +) + + +def load_digitaltwins_commands(self, _): + """ + Load CLI commands + """ + with self.command_group( + "dt", + command_type=digitaltwins_resource_ops, + resource_type=ResourceType.MGMT_RESOURCE_RESOURCES, + is_preview=True, + ) as cmd_group: + cmd_group.command("create", "create_instance") + cmd_group.command("show", "show_instance") + cmd_group.command("list", "list_instances") + cmd_group.command("delete", "delete_instance") + + with self.command_group( + "dt endpoint", + command_type=digitaltwins_resource_ops + ) as cmd_group: + cmd_group.command("show", "show_endpoint") + cmd_group.command("list", "list_endpoints") + cmd_group.command("delete", "delete_endpoint") + + with self.command_group( + "dt endpoint create", + command_type=digitaltwins_resource_ops + ) as cmd_group: + cmd_group.command("eventgrid", "add_endpoint_eventgrid") + cmd_group.command("servicebus", "add_endpoint_servicebus") + cmd_group.command("eventhub", "add_endpoint_eventhub") + + with self.command_group( + "dt route", + command_type=digitaltwins_route_ops + ) as cmd_group: + cmd_group.command("show", "show_route") + cmd_group.command("list", "list_routes") + cmd_group.command("delete", "delete_route") + cmd_group.command("create", "create_route") + + with self.command_group( + "dt role-assignment", + command_type=digitaltwins_rbac_ops + ) as cmd_group: + cmd_group.command("create", "assign_role") + cmd_group.command("delete", "remove_role") + cmd_group.command("list", "list_assignments") + + with self.command_group( + "dt twin", command_type=digitaltwins_twin_ops + ) as cmd_group: + cmd_group.command("query", "query_twins") + cmd_group.command("create", "create_twin") + cmd_group.command("show", "show_twin") + cmd_group.command("update", "update_twin") + cmd_group.command("delete", "delete_twin") + + with self.command_group( + "dt twin component", command_type=digitaltwins_twin_ops + ) as cmd_group: + cmd_group.command("show", "show_component") + cmd_group.command("update", "update_component") + + with self.command_group( + "dt twin relationship", command_type=digitaltwins_twin_ops + ) as cmd_group: + cmd_group.command("create", "create_relationship") + cmd_group.command("show", "show_relationship") + cmd_group.command("list", "list_relationships") + cmd_group.command("update", "update_relationship") + cmd_group.command("delete", "delete_relationship") + + with self.command_group( + "dt twin telemetry", command_type=digitaltwins_twin_ops + ) as cmd_group: + cmd_group.command("send", "send_telemetry") + + with self.command_group( + "dt model", command_type=digitaltwins_model_ops + ) as cmd_group: + cmd_group.command("create", "add_models") + cmd_group.command("show", "show_model") + cmd_group.command("list", "list_models") + cmd_group.command("update", "update_model") + cmd_group.command("delete", "delete_model") diff --git a/azext_iot/digitaltwins/commands_models.py b/azext_iot/digitaltwins/commands_models.py new file mode 100644 index 000000000..91f32b766 --- /dev/null +++ b/azext_iot/digitaltwins/commands_models.py @@ -0,0 +1,46 @@ +# 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.digitaltwins.providers.model import ModelProvider +from knack.log import get_logger + +logger = get_logger(__name__) + + +def add_models(cmd, name, models=None, from_directory=None, resource_group_name=None): + model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) + logger.debug("Received models input: %s", models) + return model_provider.add(models=models, from_directory=from_directory) + + +def show_model(cmd, name, model_id, definition=False, resource_group_name=None): + model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) + return model_provider.get(id=model_id, get_definition=definition) + + +def list_models( + cmd, name, definition=False, dependencies_for=None, resource_group_name=None +): + model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) + return model_provider.list( + get_definition=definition, dependencies_for=dependencies_for + ) + + +def update_model(cmd, name, model_id, decommission=None, resource_group_name=None): + if decommission is None: + logger.info("No update arguments provided. Nothing to update.") + return + + model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) + return model_provider.update( + id=model_id, decommission=decommission, + ) + + +def delete_model(cmd, name, model_id, resource_group_name=None): + model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) + return model_provider.delete(id=model_id) diff --git a/azext_iot/digitaltwins/commands_rbac.py b/azext_iot/digitaltwins/commands_rbac.py new file mode 100644 index 000000000..98526b882 --- /dev/null +++ b/azext_iot/digitaltwins/commands_rbac.py @@ -0,0 +1,39 @@ +# 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.digitaltwins.providers.resource import ResourceProvider +from knack.log import get_logger + +logger = get_logger(__name__) + + +def assign_role(cmd, name, role_type, assignee, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.assign_role( + name=name, + role_type=role_type, + assignee=assignee, + resource_group_name=resource_group_name, + ) + + +def remove_role(cmd, name, assignee=None, role_type=None, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.remove_role( + name=name, + assignee=assignee, + role_type=role_type, + resource_group_name=resource_group_name, + ) + + +def list_assignments(cmd, name, include_inherited=False, role_type=None, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.get_role_assignments( + name=name, + include_inherited=include_inherited, + resource_group_name=resource_group_name, + role_type=role_type) diff --git a/azext_iot/digitaltwins/commands_resource.py b/azext_iot/digitaltwins/commands_resource.py new file mode 100644 index 000000000..04f7f1784 --- /dev/null +++ b/azext_iot/digitaltwins/commands_resource.py @@ -0,0 +1,146 @@ +# 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.digitaltwins.providers.resource import ResourceProvider +from azext_iot.digitaltwins.common import ADTEndpointType +from knack.log import get_logger + +logger = get_logger(__name__) + + +def create_instance(cmd, name, resource_group_name, location, tags=None): + rp = ResourceProvider(cmd) + return rp.create(name=name, resource_group_name=resource_group_name, location=location, tags=tags) + + +def list_instances(cmd, resource_group_name=None): + rp = ResourceProvider(cmd) + + if not resource_group_name: + return rp.list() + return rp.list_by_resouce_group(resource_group_name) + + +def show_instance(cmd, name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.find_instance(name=name, resource_group_name=resource_group_name) + + +def delete_instance(cmd, name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.delete(name=name, resource_group_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) + + +def show_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) + + +def delete_endpoint(cmd, name, endpoint_name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.delete_endpoint(name=name, + endpoint_name=endpoint_name, + resource_group_name=resource_group_name) + + +def add_endpoint_eventgrid(cmd, name, endpoint_name, eventgrid_topic_name, + eventgrid_resource_group, resource_group_name=None, + tags=None): + return _add_endpoint_eventgrid( + cmd=cmd, + name=name, + endpoint_name=endpoint_name, + eventgrid_resource_group=eventgrid_resource_group, + eventgrid_topic_name=eventgrid_topic_name, + resource_group_name=resource_group_name, + tags=tags) + + +def _add_endpoint_eventgrid(cmd, name, endpoint_name, eventgrid_topic_name, + eventgrid_resource_group, timeout=15, resource_group_name=None, + tags=None): + rp = ResourceProvider(cmd) + return rp.add_endpoint(name=name, + resource_group_name=resource_group_name, + endpoint_name=endpoint_name, + endpoint_resource_type=ADTEndpointType.eventgridtopic, + endpoint_resource_name=eventgrid_topic_name, + endpoint_resource_group=eventgrid_resource_group, + tags=tags, + timeout=timeout) + + +def add_endpoint_servicebus(cmd, name, endpoint_name, servicebus_topic_name, + servicebus_resource_group, servicebus_policy, + servicebus_namespace, resource_group_name=None, + tags=None): + return _add_endpoint_servicebus( + cmd=cmd, + name=name, + endpoint_name=endpoint_name, + servicebus_topic_name=servicebus_topic_name, + servicebus_resource_group=servicebus_resource_group, + servicebus_policy=servicebus_policy, + servicebus_namespace=servicebus_namespace, + resource_group_name=resource_group_name, + tags=tags) + + +def _add_endpoint_servicebus(cmd, name, endpoint_name, servicebus_topic_name, + servicebus_resource_group, servicebus_policy, + servicebus_namespace, timeout=15, resource_group_name=None, + tags=None): + rp = ResourceProvider(cmd) + return rp.add_endpoint(name=name, + resource_group_name=resource_group_name, + endpoint_name=endpoint_name, + endpoint_resource_type=ADTEndpointType.servicebus, + endpoint_resource_name=servicebus_topic_name, + endpoint_resource_group=servicebus_resource_group, + endpoint_resource_namespace=servicebus_namespace, + endpoint_resource_policy=servicebus_policy, + tags=tags, + timeout=timeout) + + +def add_endpoint_eventhub(cmd, name, endpoint_name, eventhub_name, + eventhub_resource_group, eventhub_policy, + eventhub_namespace, resource_group_name=None, + tags=None): + return _add_endpoint_eventhub( + cmd=cmd, + name=name, + endpoint_name=endpoint_name, + eventhub_name=eventhub_name, + eventhub_resource_group=eventhub_resource_group, + eventhub_policy=eventhub_policy, + eventhub_namespace=eventhub_namespace, + resource_group_name=resource_group_name, + tags=tags) + + +def _add_endpoint_eventhub(cmd, name, endpoint_name, eventhub_name, + eventhub_resource_group, eventhub_policy, + eventhub_namespace, timeout=15, resource_group_name=None, + tags=None): + rp = ResourceProvider(cmd) + return rp.add_endpoint(name=name, + resource_group_name=resource_group_name, + endpoint_name=endpoint_name, + endpoint_resource_type=ADTEndpointType.eventhub, + endpoint_resource_name=eventhub_name, + endpoint_resource_group=eventhub_resource_group, + endpoint_resource_namespace=eventhub_namespace, + endpoint_resource_policy=eventhub_policy, + tags=tags, + timeout=timeout) diff --git a/azext_iot/digitaltwins/commands_routes.py b/azext_iot/digitaltwins/commands_routes.py new file mode 100644 index 000000000..1d169209d --- /dev/null +++ b/azext_iot/digitaltwins/commands_routes.py @@ -0,0 +1,30 @@ +# 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.digitaltwins.providers.route import RouteProvider +from knack.log import get_logger + +logger = get_logger(__name__) + + +def create_route(cmd, name, route_name, endpoint_name, filter="true", resource_group_name=None): + route_provider = RouteProvider(cmd=cmd, name=name, rg=resource_group_name) + return route_provider.create(route_name=route_name, endpoint_name=endpoint_name, filter=filter) + + +def show_route(cmd, name, route_name, resource_group_name=None): + route_provider = RouteProvider(cmd=cmd, name=name, rg=resource_group_name) + return route_provider.get(route_name=route_name) + + +def list_routes(cmd, name, resource_group_name=None): + route_provider = RouteProvider(cmd=cmd, name=name, rg=resource_group_name) + return route_provider.list() + + +def delete_route(cmd, name, route_name, resource_group_name=None): + route_provider = RouteProvider(cmd=cmd, name=name, rg=resource_group_name) + return route_provider.delete(route_name=route_name) diff --git a/azext_iot/digitaltwins/commands_twins.py b/azext_iot/digitaltwins/commands_twins.py new file mode 100644 index 000000000..c7a4eaba7 --- /dev/null +++ b/azext_iot/digitaltwins/commands_twins.py @@ -0,0 +1,132 @@ +# 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.digitaltwins.providers.twin import TwinProvider +from knack.log import get_logger + +logger = get_logger(__name__) + + +def query_twins(cmd, name, query_command, show_cost=False, resource_group_name=None): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.invoke_query(query=query_command, show_cost=show_cost) + + +def create_twin( + cmd, name, twin_id, model_id, properties=None, resource_group_name=None +): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.create( + twin_id=twin_id, model_id=model_id, properties=properties + ) + + +def show_twin(cmd, name, twin_id, resource_group_name=None): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.get(twin_id) + + +def update_twin(cmd, name, twin_id, json_patch, resource_group_name=None): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.update(twin_id=twin_id, json_patch=json_patch) + + +def delete_twin(cmd, name, twin_id, resource_group_name=None): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.delete(twin_id) + + +def create_relationship( + cmd, + name, + source_twin_id, + target_twin_id, + relationship_id, + relationship, + properties=None, + resource_group_name=None, +): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.add_relationship( + source_twin_id=source_twin_id, + target_twin_id=target_twin_id, + relationship_id=relationship_id, + relationship=relationship, + properties=properties, + ) + + +def show_relationship( + cmd, name, twin_id, relationship_id, resource_group_name=None, +): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.get_relationship( + twin_id=twin_id, relationship_id=relationship_id + ) + + +def update_relationship( + cmd, name, twin_id, relationship_id, json_patch, resource_group_name=None +): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.update_relationship( + twin_id=twin_id, relationship_id=relationship_id, json_patch=json_patch, + ) + + +def list_relationships( + cmd, + name, + twin_id, + incoming_relationships=False, + relationship=None, + resource_group_name=None, +): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.list_relationships( + twin_id=twin_id, + incoming_relationships=incoming_relationships, + relationship=relationship, + ) + + +def delete_relationship( + cmd, name, twin_id, relationship_id, resource_group_name=None, +): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.delete_relationship( + twin_id=twin_id, relationship_id=relationship_id + ) + + +def send_telemetry( + cmd, + name, + twin_id, + dt_id=None, + component_path=None, + telemetry=None, + resource_group_name=None, +): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.send_telemetry( + twin_id=twin_id, dt_id=dt_id, component_path=component_path, telemetry=telemetry + ) + + +def show_component(cmd, name, twin_id, component_path, resource_group_name=None): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + return twin_provider.get_component(twin_id=twin_id, component_path=component_path) + + +def update_component( + cmd, name, twin_id, component_path, json_patch, resource_group_name=None +): + twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + twin_provider.update_component( + twin_id=twin_id, component_path=component_path, json_patch=json_patch + ) + return diff --git a/azext_iot/digitaltwins/common.py b/azext_iot/digitaltwins/common.py new file mode 100644 index 000000000..cba8af492 --- /dev/null +++ b/azext_iot/digitaltwins/common.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +""" +shared: Define shared data types(enums) + +""" + +from enum import Enum + + +class ADTSkuType(Enum): + """ + ADT SKU Type. + """ + + S1 = "S1" + + +class ADTLocationType(Enum): + """ + ADT Location Type. + """ + + WestCentralUS = "westcentralus" + WestUS2 = "westus2" + EastUS2EUAP = "eastus2euap" + + +class ADTEndpointType(Enum): + """ + ADT Location Type. + """ + + eventgridtopic = "eventgridtopic" + servicebus = "servicebus" + eventhub = "eventhub" diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py new file mode 100644 index 000000000..cf2705056 --- /dev/null +++ b/azext_iot/digitaltwins/params.py @@ -0,0 +1,289 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +""" +CLI parameter definitions. +""" + +from knack.arguments import CLIArgumentType +from azure.cli.core.commands.parameters import ( + resource_group_name_type, + get_three_state_flag, +) + +depfor_type = CLIArgumentType( + options_list=["--dependencies-for"], + type=str, + nargs="+", + help="The set of models which will have their dependencies retrieved. " + "If omitted, all models are retrieved. Format is a whitespace separated list.", +) + + +def load_digitaltwins_arguments(self, _): + """ + Load CLI Args for Knack parser + """ + with self.argument_context("dt") as context: + context.argument( + "resource_group_name", + arg_type=resource_group_name_type, + help="Digital Twins instance resource group. " + "You can configure the default group using `az configure --defaults group=`.", + ) + context.argument( + "name", + options_list=["-n", "--dtn", "--dt-name"], + help="Digital Twins instance name.", + ) + context.argument( + "location", + options_list=["--location", "-l"], + help="Digital Twins instance location. " + "You can configure the default location using `az configure --defaults location=`.", + ), + context.argument( + "tags", + options_list=["--tags"], + help="Digital Twins instance tags. Property bag in key-value pairs with the following format: a=b;c=d.", + ) + context.argument( + "endpoint_name", + options_list=["--endpoint-name", "--en"], + help="Endpoint name.", + ) + context.argument( + "route_name", + options_list=["--route-name", "--rn"], + help="Event route name.", + ) + context.argument( + "filter", options_list=["--filter"], help="Event route filter.", + ) + context.argument( + "role_type", + options_list=["--role"], + help="Role name or Id.", + ) + context.argument( + "assignee", + options_list=["--assignee"], + help="Represent a user, group, or service principal. supported format: " + "object id, user sign-in name, or service principal name.", + ) + context.argument( + "model_id", + options_list=["--model-id", "--dtmi", "-m"], + help="Digital Twins model Id. Example: dtmi:example:Room;2", + ) + context.argument( + "twin_id", options_list=["--twin-id", "-t"], help="The digital twin Id.", + ) + context.argument( + "include_inherited", + options_list=["--include-inherited"], + help="Include assignments applied on parent scopes.", + arg_type=get_three_state_flag(), + ) + context.argument( + "top", + type=int, + options_list=["--top"], + help="Maximum number of elements to return.", + ) + + with self.argument_context("dt endpoint create") as context: + context.argument( + "tags", + options_list=["--tags"], + help="Digital Twins endpoint tags. Property bag in key-value pairs with the following format: a=b;c=d.", + ) + + with self.argument_context("dt endpoint create eventgrid") as context: + context.argument( + "eventgrid_topic_name", + options_list=["--eventgrid-topic", "--egt"], + help="Name of EventGrid Topic to integrate with.", + arg_group="Event Grid Topic", + ) + context.argument( + "eventgrid_resource_group", + options_list=["--eventgrid-resource-group", "--egg"], + help="Name of EventGrid Topic resource group.", + arg_group="Event Grid Topic", + ) + + with self.argument_context("dt endpoint create eventhub") as context: + context.argument( + "eventhub_name", + options_list=["--eventhub", "--eh"], + help="Name of EventHub to integrate with.", + arg_group="Event Hub", + ) + context.argument( + "eventhub_policy", + options_list=["--eventhub-policy", "--ehp"], + help="EventHub policy to use for endpoint configuration.", + arg_group="Event Hub", + ) + context.argument( + "eventhub_namespace", + options_list=["--eventhub-namespace", "--ehn"], + help="EventHub Namespace identifier.", + arg_group="Event Hub", + ) + context.argument( + "eventhub_resource_group", + options_list=["--eventhub-resource-group", "--ehg"], + help="Name of EventHub resource group.", + arg_group="Event Hub", + ) + + with self.argument_context("dt endpoint create servicebus") as context: + context.argument( + "servicebus_topic_name", + options_list=["--servicebus-topic", "--sbt"], + help="Name of ServiceBus Topic to integrate with.", + arg_group="Service Bus Topic", + ) + context.argument( + "servicebus_policy", + options_list=["--servicebus-policy", "--sbp"], + help="ServiceBus Topic policy to use for endpoint configuration.", + arg_group="Service Bus Topic", + ) + context.argument( + "servicebus_namespace", + options_list=["--servicebus-namespace", "--sbn"], + help="ServiceBus Namespace identifier.", + arg_group="Service Bus Topic", + ) + context.argument( + "servicebus_resource_group", + options_list=["--servicebus-resource-group", "--sbg"], + help="Name of ServiceBus resource group.", + arg_group="Service Bus Topic", + ) + + with self.argument_context("dt twin") as context: + context.argument( + "query_command", + options_list=["--query-command", "-q"], + help="User query to be executed.", + ) + context.argument( + "show_cost", + options_list=["--show-cost", "--cost"], + help="Calculates and shows the query charge.", + arg_type=get_three_state_flag(), + ) + context.argument( + "relationship_id", + options_list=["--relationship-id", "-r"], + help="Relationship Id.", + ) + context.argument( + "source_twin_id", + options_list=["--source-twin-id", "--source", "-s"], + help="The source twin Id for a relationship.", + ) + context.argument( + "target_twin_id", + options_list=["--target-twin-id", "--target", "-t"], + help="The target twin Id for a relationship.", + ) + context.argument( + "relationship", + options_list=["--relationship", "--kind"], + help="Relationship name or kind. For example: 'contains'", + ) + context.argument( + "json_patch", + options_list=["--json-patch", "--patch"], + help="An update specification described by JSON-patch. " + "Updates to property values and $model elements may happen in the same request. " + "Operations are limited to add, replace and remove. Provide file path or inline JSON.", + ) + context.argument( + "component_path", + options_list=["--component"], + help="The path to the DTDL component." + ) + + with self.argument_context("dt twin create") as context: + context.argument( + "properties", + options_list=["--properties", "-p"], + help="Initial property values for instantiating a digital twin or related components. " + "Provide file path or inline JSON.", + ) + + with self.argument_context("dt twin telemetry") as context: + context.argument( + "telemetry", + options_list=["--telemetry"], + help="Inline telemetry JSON or file path to telemetry JSON. Default payload is an empty object: {}", + ) + context.argument( + "dt_id", + options_list=["--dt-id"], + help="A unique message identifier (in the scope of the digital twin id) that is commonly used " + "for de-duplicating messages. If no value is provided a GUID is automatically generated.", + ) + context.argument( + "component_path", + options_list=["--component"], + help="The path to the DTDL component. If set telemetry will be emitted on behalf of the component." + ) + + with self.argument_context("dt twin relationship create") as context: + context.argument( + "properties", + options_list=["--properties", "-p"], + help="Initial property values for instantiating a digital twin relationship. Provide file path or inline JSON.", + ) + + with self.argument_context("dt twin relationship list") as context: + context.argument( + "incoming_relationships", + options_list=["--incoming"], + help="Retrieves all incoming relationships for a digital twin.", + arg_type=get_three_state_flag(), + ) + context.argument( + "relationship", + options_list=["--relationship", "--kind"], + help="Filter result by the kind of relationship.", + ) + + with self.argument_context("dt model") as context: + context.argument( + "from_directory", + options_list=["--from-directory", "--fd"], + help="The directory JSON model files will be parsed from.", + arg_group="Models Input", + ) + context.argument( + "models", + options_list=["--models"], + help="Inline model JSON or file path to model JSON.", + arg_group="Models Input", + ) + context.argument( + "definition", + options_list=["--definition", "--def"], + arg_type=get_three_state_flag(), + help="The operation will retrieve the model definition.", + ) + context.argument( + "decommission", + options_list=["--decommission"], + arg_type=get_three_state_flag(), + help="Indicates intent to decommission a target model.", + ) + context.argument( + "dependencies_for", arg_type=depfor_type, + ) diff --git a/azext_iot/digitaltwins/providers/__init__.py b/azext_iot/digitaltwins/providers/__init__.py new file mode 100644 index 000000000..f494a0333 --- /dev/null +++ b/azext_iot/digitaltwins/providers/__init__.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.sdk.digitaltwins_arm import AzureDigitalTwinsManagementClient +from azext_iot.sdk.digitaltwins_arm.models import ErrorResponseException +from msrestazure.azure_exceptions import CloudError + +__all__ = [ + "digitaltwins_service_factory", + "DigitalTwinsResourceManager", + "CloudError", + "ErrorResponseException", +] + + +def digitaltwins_service_factory(cli_ctx, *_): + """ + Factory for importing deps and getting service client resources. + + Args: + cli_ctx (knack.cli.CLI): CLI context. + *_ : all other args ignored. + + Returns: + iot_hub_resource (IotHubClient.iot_hub_resource): operational resource for + working with IoT Hub. + """ + from azure.cli.core.commands.client_factory import get_mgmt_service_client + + return get_mgmt_service_client(cli_ctx, AzureDigitalTwinsManagementClient) + + +class DigitalTwinsResourceManager(object): + def __init__(self, cmd): + assert cmd + self.cmd = cmd + + def get_mgmt_sdk(self): + return digitaltwins_service_factory(self.cmd.cli_ctx) diff --git a/azext_iot/digitaltwins/providers/auth.py b/azext_iot/digitaltwins/providers/auth.py new file mode 100644 index 000000000..509ee8016 --- /dev/null +++ b/azext_iot/digitaltwins/providers/auth.py @@ -0,0 +1,59 @@ +# 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 DigitalTwinAuthentication(Authentication): + """ + Shared Access Signature authorization 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(DigitalTwinAuthentication, 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/digitaltwins/providers/base.py b/azext_iot/digitaltwins/providers/base.py new file mode 100644 index 000000000..1909c2e40 --- /dev/null +++ b/azext_iot/digitaltwins/providers/base.py @@ -0,0 +1,37 @@ +# 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.digitaltwins.providers.resource import ResourceProvider +from azext_iot.digitaltwins.providers.auth import DigitalTwinAuthentication +from azext_iot.sdk.digitaltwins import AzureDigitalTwinsAPI +from azext_iot.sdk.digitaltwins.models import ErrorResponseException +from azext_iot.constants import DIGITALTWINS_RESOURCE_ID +from knack.cli import CLIError + +__all__ = ["DigitalTwinsProvider", "ErrorResponseException"] + + +class DigitalTwinsProvider(object): + def __init__(self, cmd, name, rg=None): + assert cmd + assert name + + self.cmd = cmd + self.name = name + self.rg = rg + self.resource_id = DIGITALTWINS_RESOURCE_ID + self.rp = ResourceProvider(self.cmd) + + def _get_endpoint(self): + instance = self.rp.find_instance(name=self.name, resource_group_name=self.rg) + host_name = instance.host_name + if not host_name: + raise CLIError("Retrieved hostName was null which is invalid.") + return "https://{}".format(instance.host_name) + + def get_sdk(self): + creds = DigitalTwinAuthentication(cmd=self.cmd, resource_id=self.resource_id) + return AzureDigitalTwinsAPI(base_url=self._get_endpoint(), credentials=creds) diff --git a/azext_iot/digitaltwins/providers/generic.py b/azext_iot/digitaltwins/providers/generic.py new file mode 100644 index 000000000..84903af7a --- /dev/null +++ b/azext_iot/digitaltwins/providers/generic.py @@ -0,0 +1,43 @@ +# 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. +# -------------------------------------------------------------------------------------------- + + +# Experimental - depends on consistency of APIs +def accumulate_result(method, token_name="continuationToken", token_arg_name="continuation_token", values_name="items", **kwargs): + result_accumulator = [] + + nextlink = None + token_keyword = {token_arg_name: nextlink} + + # TODO: Genericize + query_cost_sum = 0 + + while True: + response = method(raw=True, **token_keyword, **kwargs).response + headers = response.headers + if headers: + query_charge = headers.get("query-charge") + if query_charge: + query_cost_sum = query_cost_sum + float(query_charge) + + result = response.json() + if result and result.get(values_name): + result_values = result.get(values_name) + result_accumulator.extend(result_values) + nextlink = result.get(token_name) + if not nextlink: + break + token_keyword[token_arg_name] = nextlink + else: + break + + return result_accumulator, query_cost_sum + + +def remove_prefix(text, prefix): + if text.startswith(prefix): + return text[len(prefix):] + return text diff --git a/azext_iot/digitaltwins/providers/model.py b/azext_iot/digitaltwins/providers/model.py new file mode 100644 index 000000000..f7b2a9ec0 --- /dev/null +++ b/azext_iot/digitaltwins/providers/model.py @@ -0,0 +1,110 @@ +# 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 azext_iot.common.utility import process_json_arg, scantree, unpack_msrest_error +from azext_iot.digitaltwins.providers.base import DigitalTwinsProvider +from azext_iot.digitaltwins.providers import ErrorResponseException +from knack.log import get_logger +from knack.util import CLIError + +logger = get_logger(__name__) + + +class ModelProvider(DigitalTwinsProvider): + def __init__(self, cmd, name, rg=None): + super(ModelProvider, self).__init__( + cmd=cmd, name=name, rg=rg, + ) + self.model_sdk = self.get_sdk().digital_twin_models + + def add(self, models=None, from_directory=None): + if not any([models, from_directory]): + raise CLIError("Provide either --models or --from-directory.") + + # If both arguments are provided. --models wins. + payload = [] + if models: + models_result = process_json_arg(content=models, argument_name="models") + + # TODO: + if isinstance(models_result, list): + payload.extend(models_result) + elif isinstance(models_result, dict): + payload.append(models_result) + + elif from_directory: + payload = self._process_directory(from_directory=from_directory) + + logger.info("Models payload %s", json.dumps(payload)) + + # TODO: Not standard - have to revisit. + response = self.model_sdk.add(payload) + if response.status_code not in [200, 201]: + error_text = response.text + if response.status_code == 403 and not error_text: + error_text = "Current principal access is forbidden. Please validate rbac role assignments." + else: + try: + error_text = response.json() + except Exception: + pass + raise CLIError(error_text) + + return response.json() + + def _process_directory(self, from_directory): + logger.debug( + "Documents contained in directory: {}, processing...".format(from_directory) + ) + payload = [] + for entry in scantree(from_directory): + if all( + [not entry.name.endswith(".json"), not entry.name.endswith(".dtdl")] + ): + logger.debug( + "Skipping {} - model file must end with .json or .dtdl".format( + entry.path + ) + ) + continue + entry_json = process_json_arg(content=entry.path, argument_name=entry.name) + payload.append(entry_json) + + return payload + + def get(self, id, get_definition=False): + return self.model_sdk.get_by_id( + id=id, include_model_definition=get_definition, raw=True + ).response.json() + + def list( + self, get_definition=False, dependencies_for=None, top=None + ): # top is guarded for int() in arg def + from azext_iot.sdk.digitaltwins.models import DigitalTwinModelsListOptions + + list_options = DigitalTwinModelsListOptions(max_item_count=top) + + return self.model_sdk.list( + dependencies_for=dependencies_for, + include_model_definition=get_definition, + digital_twin_models_list_options=list_options, + ) + + def update(self, id, decommission: bool): + patched_model = [ + {"op": "replace", "path": "/decommissioned", "value": decommission} + ] + + # Does not return model object upon updating + self.model_sdk.update(id=id, update_model=patched_model) + return self.get(id=id) + + def delete(self, id: str): + try: + return self.model_sdk.delete(id=id) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/digitaltwins/providers/rbac.py b/azext_iot/digitaltwins/providers/rbac.py new file mode 100644 index 000000000..3c7a9a50a --- /dev/null +++ b/azext_iot/digitaltwins/providers/rbac.py @@ -0,0 +1,56 @@ +# 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.common.embedded_cli import EmbeddedCLI +from knack.util import CLIError + + +class RbacProvider(object): + def __init__(self): + self.cli = EmbeddedCLI() + + def list_assignments(self, dt_scope, include_inherited=False, role_type=None): + include_inherited_flag = "" + filter_role_type = "" + + if include_inherited: + include_inherited_flag = "--include-inherited" + + if role_type: + filter_role_type = "--role '{}'".format(role_type) + + list_op = self.cli.invoke("role assignment list --scope '{}' {} {}".format( + dt_scope, filter_role_type, include_inherited_flag)) + + if not list_op.success(): + raise CLIError("Unable to determine assignments.") + + return list_op.as_json() + + def assign_role(self, dt_scope, assignee, role_type): + assign_op = self.cli.invoke( + "role assignment create --scope '{}' --role '{}' --assignee '{}'".format( + dt_scope, role_type, assignee + ) + ) + if not assign_op.success(): + raise CLIError("Unable to assign role.") + + return assign_op.as_json() + + def remove_role(self, dt_scope, assignee, role_type=None): + filter_role_type = "" + if role_type: + filter_role_type = "--role '{}'".format(role_type) + + delete_op = self.cli.invoke( + "role assignment delete --scope '{}' --assignee '{}' {}".format( + dt_scope, assignee, filter_role_type + ) + ) + if not delete_op.success(): + raise CLIError("Unable to remove role assignment.") + return diff --git a/azext_iot/digitaltwins/providers/resource.py b/azext_iot/digitaltwins/providers/resource.py new file mode 100644 index 000000000..fdaf23ef0 --- /dev/null +++ b/azext_iot/digitaltwins/providers/resource.py @@ -0,0 +1,333 @@ +# 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.digitaltwins.providers import ( + DigitalTwinsResourceManager, + CloudError, + ErrorResponseException, +) +from azext_iot.digitaltwins.providers.rbac import RbacProvider +from azext_iot.common.utility import validate_key_value_pairs, unpack_msrest_error +from knack.util import CLIError + + +class ResourceProvider(DigitalTwinsResourceManager): + def __init__(self, cmd): + super(ResourceProvider, self).__init__(cmd=cmd) + self.mgmt_sdk = self.get_mgmt_sdk() + self.rbac = RbacProvider() + + def create(self, name, resource_group_name, location, tags=None, timeout=15): + if tags: + tags = validate_key_value_pairs(tags) + + try: + return self.mgmt_sdk.digital_twins.create_or_update( + resource_name=name, + resource_group_name=resource_group_name, + location=location, + tags=tags, + long_running_operation_timeout=timeout, + ) + except CloudError as e: + raise e + except ErrorResponseException as err: + raise CLIError(unpack_msrest_error(err)) + + def list(self): + try: + return self.mgmt_sdk.digital_twins.list() + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def list_by_resouce_group(self, resource_group_name): + try: + return self.mgmt_sdk.digital_twins.list_by_resource_group( + resource_group_name=resource_group_name + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def get(self, name, resource_group_name): + try: + return self.mgmt_sdk.digital_twins.get( + resource_name=name, resource_group_name=resource_group_name + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def find_instance(self, name, resource_group_name=None): + 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)) + + dt_collection_pager = self.list() + dt_collection = [] + try: + while True: + dt_collection.extend(dt_collection_pager.advance_page()) + except StopIteration: + pass + + compare_name = name.lower() + filter_result = [ + instance + for instance in dt_collection + if instance.name.lower() == compare_name + ] + + if filter_result: + if len(filter_result) > 1: + raise CLIError( + "Ambiguous DT instance name. Please include the DT instance resource group." + ) + return filter_result[0] + + raise CLIError( + "DT instance: '{}' not found by auto-discovery. " + "Provide resource group via -g for direct lookup.".format(name) + ) + + def get_rg(self, dt_instance): + dt_scope = dt_instance.id + split_decomp = dt_scope.split("/") + res_g = split_decomp[4] + return res_g + + def delete(self, name, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + # TODO: Don't return result. Issue in service related to status code and polling. + try: + self.mgmt_sdk.digital_twins.delete( + resource_name=name, + resource_group_name=resource_group_name, + polling=False, + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + # RBAC + + def get_role_assignments( + self, name, include_inherited=False, role_type=None, resource_group_name=None + ): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + return self.rbac.list_assignments( + dt_scope=target_instance.id, + include_inherited=include_inherited, + role_type=role_type, + ) + + def assign_role(self, name, role_type, assignee, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + return self.rbac.assign_role( + dt_scope=target_instance.id, assignee=assignee, role_type=role_type + ) + + def remove_role(self, name, assignee, role_type=None, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + return self.rbac.remove_role( + dt_scope=target_instance.id, assignee=assignee, role_type=role_type + ) + + # Endpoints + + def get_endpoint(self, name, endpoint_name, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + try: + return self.mgmt_sdk.digital_twins_endpoint.get( + resource_name=target_instance.name, + endpoint_name=endpoint_name, + resource_group_name=resource_group_name, + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def list_endpoints(self, name, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + try: + return self.mgmt_sdk.digital_twins_endpoint.list( + resource_name=target_instance.name, + resource_group_name=resource_group_name, + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + # TODO: Polling issue related to mismatched status codes. + def delete_endpoint(self, name, endpoint_name, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + try: + # TODO: Polling set to false + return self.mgmt_sdk.digital_twins_endpoint.delete( + resource_name=target_instance.name, + endpoint_name=endpoint_name, + resource_group_name=resource_group_name, + polling=False, + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def add_endpoint( + self, + name, + endpoint_name, + endpoint_resource_type, + endpoint_resource_name, + endpoint_resource_group, + endpoint_resource_policy=None, + endpoint_resource_namespace=None, + tags=None, + resource_group_name=None, + timeout=20, + ): + from azext_iot.common.embedded_cli import EmbeddedCLI + from azext_iot.digitaltwins.common import ADTEndpointType + + requires_policy = [ADTEndpointType.eventhub, ADTEndpointType.servicebus] + if endpoint_resource_type in requires_policy: + if not endpoint_resource_policy: + raise CLIError( + "Endpoint resources of type {} require a policy name.".format( + " or ".join(map(str, requires_policy)) + ) + ) + + if not endpoint_resource_namespace: + raise CLIError( + "Endpoint resources of type {} require a namespace.".format( + " or ".join(map(str, requires_policy)) + ) + ) + + if tags: + tags = validate_key_value_pairs(tags) + + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + payload = {"tags": tags} + cli = EmbeddedCLI() + error_prefix = "Could not create ADT instance endpoint. Unable to retrieve" + if endpoint_resource_type == ADTEndpointType.eventgridtopic: + eg_topic_keys_op = cli.invoke( + "eventgrid topic key list -n {} -g {}".format( + endpoint_resource_name, endpoint_resource_group + ) + ) + if not eg_topic_keys_op.success(): + raise CLIError("{} Event Grid topic keys.".format(error_prefix)) + eg_topic_keys = eg_topic_keys_op.as_json() + + eg_topic_endpoint_op = cli.invoke( + "eventgrid topic show -n {} -g {}".format( + endpoint_resource_name, endpoint_resource_group + ) + ) + if not eg_topic_endpoint_op.success(): + raise CLIError("{} Event Grid topic endpoint.".format(error_prefix)) + eg_topic_endpoint = eg_topic_endpoint_op.as_json() + + payload["endpointType"] = "EventGrid" + payload["accessKey1"] = eg_topic_keys["key1"] + payload["accessKey2"] = eg_topic_keys["key2"] + payload["TopicEndpoint"] = eg_topic_endpoint["endpoint"] + + elif endpoint_resource_type == ADTEndpointType.servicebus: + sb_topic_keys_op = cli.invoke( + "servicebus topic authorization-rule keys list -n {} " + "--namespace-name {} -g {} --topic-name {}".format( + endpoint_resource_policy, + endpoint_resource_namespace, + endpoint_resource_group, + endpoint_resource_name, + ) + ) + if not sb_topic_keys_op.success(): + raise CLIError("{} Service Bus topic keys.".format(error_prefix)) + sb_topic_keys = sb_topic_keys_op.as_json() + + payload["endpointType"] = "ServiceBus" + payload["primaryConnectionString"] = sb_topic_keys[ + "primaryConnectionString" + ] + payload["secondaryConnectionString"] = sb_topic_keys[ + "secondaryConnectionString" + ] + + elif endpoint_resource_type == ADTEndpointType.eventhub: + eventhub_topic_keys_op = cli.invoke( + "eventhubs eventhub authorization-rule keys list -n {} " + "--namespace-name {} -g {} --eventhub-name {}".format( + endpoint_resource_policy, + endpoint_resource_namespace, + endpoint_resource_group, + endpoint_resource_name, + ) + ) + if not eventhub_topic_keys_op.success(): + raise CLIError("{} Event Hub keys.".format(error_prefix)) + eventhub_topic_keys = eventhub_topic_keys_op.as_json() + + payload["endpointType"] = "EventHub" + payload["connectionString-PrimaryKey"] = eventhub_topic_keys[ + "primaryConnectionString" + ] + payload["connectionString-SecondaryKey"] = eventhub_topic_keys[ + "secondaryConnectionString" + ] + + properties = {"properties": payload} + + try: + return self.mgmt_sdk.digital_twins_endpoint.create_or_update( + resource_name=target_instance.name, + resource_group_name=resource_group_name, + endpoint_name=endpoint_name, + properties=properties, + long_running_operation_timeout=timeout, + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/digitaltwins/providers/route.py b/azext_iot/digitaltwins/providers/route.py new file mode 100644 index 000000000..35ea88e10 --- /dev/null +++ b/azext_iot/digitaltwins/providers/route.py @@ -0,0 +1,58 @@ +# 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.common.utility import unpack_msrest_error +from azext_iot.digitaltwins.providers.base import DigitalTwinsProvider +from azext_iot.digitaltwins.providers import ErrorResponseException +from knack.log import get_logger +from knack.util import CLIError + +logger = get_logger(__name__) + + +class RouteProvider(DigitalTwinsProvider): + def __init__(self, cmd, name, rg=None): + super(RouteProvider, self).__init__( + cmd=cmd, + name=name, + rg=rg + ) + self.sdk = self.get_sdk().event_routes + + def get(self, route_name): + try: + return self.sdk.get_by_id(route_name) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def list(self, top=None): # top is guarded for int() in arg def + from azext_iot.sdk.digitaltwins.models import EventRoutesListOptions + list_options = EventRoutesListOptions(max_item_count=top) + + try: + return self.sdk.list( + event_routes_list_options=list_options, + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def create(self, route_name, endpoint_name, filter=None): + if not filter: + filter = "true" + + # TODO: Adding routes does not return an object + try: + self.sdk.add(id=route_name, endpoint_id=endpoint_name, filter=filter) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + return self.get(route_name=route_name) + + def delete(self, route_name): + try: + return self.sdk.delete(id=route_name) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/digitaltwins/providers/twin.py b/azext_iot/digitaltwins/providers/twin.py new file mode 100644 index 000000000..80d72681c --- /dev/null +++ b/azext_iot/digitaltwins/providers/twin.py @@ -0,0 +1,251 @@ +# 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 azext_iot.digitaltwins.providers.base import ( + DigitalTwinsProvider, + ErrorResponseException, +) +from azext_iot.digitaltwins.providers.model import ModelProvider +from azext_iot.common.utility import process_json_arg, unpack_msrest_error +from knack.log import get_logger +from knack.util import CLIError + +logger = get_logger(__name__) + + +class TwinProvider(DigitalTwinsProvider): + def __init__(self, cmd, name, rg=None): + super(TwinProvider, self).__init__( + cmd=cmd, name=name, rg=rg, + ) + self.model_provider = ModelProvider(cmd=cmd, name=name, rg=rg) + self.query_sdk = self.get_sdk().query + self.twins_sdk = self.get_sdk().digital_twins + + def invoke_query(self, query, show_cost): + from azext_iot.digitaltwins.providers.generic import accumulate_result + + accumulated_result, cost = accumulate_result( + self.query_sdk.query_twins, + values_name="items", + token_name="continuationToken", + token_arg_name="continuation_token", + query=query, + ) + + query_result = {} + query_result["result"] = accumulated_result + if show_cost: + query_result["cost"] = cost + + return query_result + + def create(self, twin_id, model_id, properties=None): + target_model = self.model_provider.get(id=model_id) + twin_request = { + "$dtId": twin_id, + "$metadata": {"$model": target_model["id"], "$kind": "DigitalTwin"}, + } + + if properties: + properties = process_json_arg( + content=properties, argument_name="properties" + ) + twin_request.update(properties) + + logger.info("Twin payload %s", json.dumps(twin_request)) + + try: + return self.twins_sdk.add(id=twin_id, twin=twin_request, if_none_match="*") + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def get(self, twin_id): + try: + return self.twins_sdk.get_by_id(id=twin_id, raw=True).response.json() + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def update(self, twin_id, json_patch): + json_patch = process_json_arg(content=json_patch, argument_name="json-patch") + + json_patch_collection = [] + if isinstance(json_patch, dict): + json_patch_collection.append(json_patch) + if isinstance(json_patch, list): + json_patch_collection.extend(json_patch) + + logger.info("Patch payload %s", json.dumps(json_patch_collection)) + + try: + self.twins_sdk.update( + id=twin_id, patch_document=json_patch_collection, if_match="*", raw=True + ) + return self.get(twin_id=twin_id) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def delete(self, twin_id): + # Not a json response + try: + self.twins_sdk.delete(id=twin_id, if_match="*", raw=True) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def add_relationship( + self, + source_twin_id, + target_twin_id, + relationship_id, + relationship, + properties=None, + ): + relationship_request = { + "$targetId": target_twin_id, + "$relationshipName": relationship, + } + + if properties: + properties = process_json_arg( + content=properties, argument_name="properties" + ) + relationship_request.update(properties) + + logger.info("Relationship payload %s", json.dumps(relationship_request)) + return self.twins_sdk.add_relationship( + id=source_twin_id, + relationship_id=relationship_id, + relationship=relationship_request, + if_none_match="*", + raw=True, + ).response.json() + + def get_relationship(self, twin_id, relationship_id): + return self.twins_sdk.get_relationship_by_id( + id=twin_id, relationship_id=relationship_id, raw=True + ).response.json() + + def list_relationships( + self, twin_id, incoming_relationships=False, relationship=None + ): + if not incoming_relationships: + return self.twins_sdk.list_relationships( + id=twin_id, relationship_name=relationship + ) + + incoming_pager = self.twins_sdk.list_incoming_relationships(id=twin_id) + + incoming_result = [] + try: + while True: + incoming_result.extend(incoming_pager.advance_page()) + except StopIteration: + pass + + if relationship: + incoming_result = [ + edge + for edge in incoming_result + if edge.relationship_name and edge.relationship_name == relationship + ] + + return incoming_result + + def update_relationship(self, twin_id, relationship_id, json_patch): + json_patch = process_json_arg(content=json_patch, argument_name="json-patch") + + json_patch_collection = [] + if isinstance(json_patch, dict): + json_patch_collection.append(json_patch) + if isinstance(json_patch, list): + json_patch_collection.extend(json_patch) + + logger.info("Patch payload %s", json.dumps(json_patch_collection)) + + try: + self.twins_sdk.update_relationship( + id=twin_id, + relationship_id=relationship_id, + patch_document=json_patch_collection, + if_match="*", + ) + return self.get_relationship( + twin_id=twin_id, relationship_id=relationship_id + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def delete_relationship(self, twin_id, relationship_id): + try: + self.twins_sdk.delete_relationship( + id=twin_id, relationship_id=relationship_id, if_match="*" + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def get_component(self, twin_id, component_path): + return self.twins_sdk.get_component( + id=twin_id, component_path=component_path, raw=True + ).response.json() + + def update_component(self, twin_id, component_path, json_patch): + json_patch = process_json_arg(content=json_patch, argument_name="json-patch") + + json_patch_collection = [] + if isinstance(json_patch, dict): + json_patch_collection.append(json_patch) + if isinstance(json_patch, list): + json_patch_collection.extend(json_patch) + + logger.info("Patch payload %s", json.dumps(json_patch_collection)) + + try: + # TODO: API does not return response + self.twins_sdk.update_component( + id=twin_id, + component_path=component_path, + patch_document=json_patch_collection, + if_match="*", + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def send_telemetry(self, twin_id, telemetry=None, dt_id=None, component_path=None): + from uuid import uuid4 + from datetime import datetime, timezone + + local_time = datetime.now(timezone.utc).astimezone() + dt_timestamp = local_time.isoformat() + + telemetry_request = {} + + if telemetry: + telemetry = process_json_arg(content=telemetry, argument_name="telemetry") + else: + telemetry = {} + + telemetry_request.update(telemetry) + + logger.info("Telemetry payload: {}".format(json.dumps(telemetry_request))) + if not dt_id: + dt_id = str(uuid4()) + + if component_path: + self.twins_sdk.send_component_telemetry( + id=twin_id, + dt_id=dt_id, + dt_timestamp=dt_timestamp, + component_path=component_path, + telemetry=telemetry_request, + ) + + self.twins_sdk.send_telemetry( + id=twin_id, + dt_id=dt_id, + dt_timestamp=dt_timestamp, + telemetry=telemetry_request, + ) diff --git a/azext_iot/sdk/digitaltwins/__init__.py b/azext_iot/sdk/digitaltwins/__init__.py new file mode 100644 index 000000000..bd6770159 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .azure_digital_twins_api import AzureDigitalTwinsAPI +from .version import VERSION + +__all__ = ['AzureDigitalTwinsAPI'] + +__version__ = VERSION + diff --git a/azext_iot/sdk/digitaltwins/azure_digital_twins_api.py b/azext_iot/sdk/digitaltwins/azure_digital_twins_api.py new file mode 100644 index 000000000..77d3012e6 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/azure_digital_twins_api.py @@ -0,0 +1,90 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.service_client import SDKClient +from msrest import Serializer, Deserializer +from msrestazure import AzureConfiguration +from .version import VERSION +from .operations.digital_twin_models_operations import DigitalTwinModelsOperations +from .operations.query_operations import QueryOperations +from .operations.digital_twins_operations import DigitalTwinsOperations +from .operations.event_routes_operations import EventRoutesOperations +from . import models +from azext_iot.constants import USER_AGENT + + +class AzureDigitalTwinsAPIConfiguration(AzureConfiguration): + """Configuration for AzureDigitalTwinsAPI + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if not base_url: + base_url = 'http://localhost' + + super(AzureDigitalTwinsAPIConfiguration, self).__init__(base_url) + + self.add_user_agent('azuredigitaltwinsapi/{}'.format(VERSION)) + self.add_user_agent(USER_AGENT) + + self.credentials = credentials + + +class AzureDigitalTwinsAPI(SDKClient): + """A service for managing and querying digital twins and digital twin models. + + :ivar config: Configuration for client. + :vartype config: AzureDigitalTwinsAPIConfiguration + + :ivar digital_twin_models: DigitalTwinModels operations + :vartype digital_twin_models: digitaltwins.operations.DigitalTwinModelsOperations + :ivar query: Query operations + :vartype query: digitaltwins.operations.QueryOperations + :ivar digital_twins: DigitalTwins operations + :vartype digital_twins: digitaltwins.operations.DigitalTwinsOperations + :ivar event_routes: EventRoutes operations + :vartype event_routes: digitaltwins.operations.EventRoutesOperations + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + self.config = AzureDigitalTwinsAPIConfiguration(credentials, base_url) + super(AzureDigitalTwinsAPI, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2020-05-31-preview' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + self.digital_twin_models = DigitalTwinModelsOperations( + self._client, self.config, self._serialize, self._deserialize) + self.query = QueryOperations( + self._client, self.config, self._serialize, self._deserialize) + self.digital_twins = DigitalTwinsOperations( + self._client, self.config, self._serialize, self._deserialize) + self.event_routes = EventRoutesOperations( + self._client, self.config, self._serialize, self._deserialize) diff --git a/azext_iot/sdk/digitaltwins/models/__init__.py b/azext_iot/sdk/digitaltwins/models/__init__.py new file mode 100644 index 000000000..62848fc83 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/__init__.py @@ -0,0 +1,54 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +try: + from .event_route_py3 import EventRoute + from .model_data_py3 import ModelData + from .incoming_relationship_py3 import IncomingRelationship + from .query_specification_py3 import QuerySpecification + from .query_result_py3 import QueryResult + from .inner_error_py3 import InnerError + from .error_py3 import Error + from .error_response_py3 import ErrorResponse, ErrorResponseException + from .digital_twin_models_list_options_py3 import DigitalTwinModelsListOptions + from .event_routes_list_options_py3 import EventRoutesListOptions +except (SyntaxError, ImportError): + from .event_route import EventRoute + from .model_data import ModelData + from .incoming_relationship import IncomingRelationship + from .query_specification import QuerySpecification + from .query_result import QueryResult + from .inner_error import InnerError + from .error import Error + from .error_response import ErrorResponse, ErrorResponseException + from .digital_twin_models_list_options import DigitalTwinModelsListOptions + from .event_routes_list_options import EventRoutesListOptions +from .model_data_paged import ModelDataPaged +from .object_paged import ObjectPaged +from .incoming_relationship_paged import IncomingRelationshipPaged +from .event_route_paged import EventRoutePaged + +__all__ = [ + 'EventRoute', + 'ModelData', + 'IncomingRelationship', + 'QuerySpecification', + 'QueryResult', + 'InnerError', + 'Error', + 'ErrorResponse', 'ErrorResponseException', + 'DigitalTwinModelsListOptions', + 'EventRoutesListOptions', + 'ModelDataPaged', + 'ObjectPaged', + 'IncomingRelationshipPaged', + 'EventRoutePaged', +] diff --git a/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options.py b/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options.py new file mode 100644 index 000000000..dff617960 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options.py @@ -0,0 +1,30 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsListOptions(Model): + """Additional parameters for list operation. + + :param max_item_count: The maximum number of items to retrieve per + request. The server may choose to return less than the requested max. + Default value: -1 . + :type max_item_count: int + """ + + _attribute_map = { + 'max_item_count': {'key': '', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinModelsListOptions, self).__init__(**kwargs) + self.max_item_count = kwargs.get('max_item_count', -1) diff --git a/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options_py3.py b/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options_py3.py new file mode 100644 index 000000000..56a13018e --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options_py3.py @@ -0,0 +1,30 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsListOptions(Model): + """Additional parameters for list operation. + + :param max_item_count: The maximum number of items to retrieve per + request. The server may choose to return less than the requested max. + Default value: -1 . + :type max_item_count: int + """ + + _attribute_map = { + 'max_item_count': {'key': '', 'type': 'int'}, + } + + def __init__(self, *, max_item_count: int=-1, **kwargs) -> None: + super(DigitalTwinModelsListOptions, self).__init__(**kwargs) + self.max_item_count = max_item_count diff --git a/azext_iot/sdk/digitaltwins/models/error.py b/azext_iot/sdk/digitaltwins/models/error.py new file mode 100644 index 000000000..be8f664d8 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/error.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Error(Model): + """Error definition. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar code: Service specific error code which serves as the substatus for + the HTTP error code. + :vartype code: str + :ivar message: A human-readable representation of the error. + :vartype message: str + :ivar details: Internal error details. + :vartype details: list[~digitaltwins.models.Error] + :param innererror: An object containing more specific information than the + current object about the error. + :type innererror: ~digitaltwins.models.InnerError + """ + + _validation = { + 'code': {'readonly': True}, + 'message': {'readonly': True}, + 'details': {'readonly': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'details': {'key': 'details', 'type': '[Error]'}, + 'innererror': {'key': 'innererror', 'type': 'InnerError'}, + } + + def __init__(self, **kwargs): + super(Error, self).__init__(**kwargs) + self.code = None + self.message = None + self.details = None + self.innererror = kwargs.get('innererror', None) diff --git a/azext_iot/sdk/digitaltwins/models/error_py3.py b/azext_iot/sdk/digitaltwins/models/error_py3.py new file mode 100644 index 000000000..caf7f5d51 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/error_py3.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Error(Model): + """Error definition. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar code: Service specific error code which serves as the substatus for + the HTTP error code. + :vartype code: str + :ivar message: A human-readable representation of the error. + :vartype message: str + :ivar details: Internal error details. + :vartype details: list[~digitaltwins.models.Error] + :param innererror: An object containing more specific information than the + current object about the error. + :type innererror: ~digitaltwins.models.InnerError + """ + + _validation = { + 'code': {'readonly': True}, + 'message': {'readonly': True}, + 'details': {'readonly': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'details': {'key': 'details', 'type': '[Error]'}, + 'innererror': {'key': 'innererror', 'type': 'InnerError'}, + } + + def __init__(self, *, innererror=None, **kwargs) -> None: + super(Error, self).__init__(**kwargs) + self.code = None + self.message = None + self.details = None + self.innererror = innererror diff --git a/azext_iot/sdk/digitaltwins/models/error_response.py b/azext_iot/sdk/digitaltwins/models/error_response.py new file mode 100644 index 000000000..3e179610e --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/error_response.py @@ -0,0 +1,41 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class ErrorResponse(Model): + """Error response. + + :param error: The error details. + :type error: ~digitaltwins.models.Error + """ + + _attribute_map = { + 'error': {'key': 'error', 'type': 'Error'}, + } + + def __init__(self, **kwargs): + super(ErrorResponse, self).__init__(**kwargs) + self.error = kwargs.get('error', None) + + +class ErrorResponseException(HttpOperationError): + """Server responsed with exception of type: 'ErrorResponse'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) diff --git a/azext_iot/sdk/digitaltwins/models/error_response_py3.py b/azext_iot/sdk/digitaltwins/models/error_response_py3.py new file mode 100644 index 000000000..89ddba1fb --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/error_response_py3.py @@ -0,0 +1,41 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class ErrorResponse(Model): + """Error response. + + :param error: The error details. + :type error: ~digitaltwins.models.Error + """ + + _attribute_map = { + 'error': {'key': 'error', 'type': 'Error'}, + } + + def __init__(self, *, error=None, **kwargs) -> None: + super(ErrorResponse, self).__init__(**kwargs) + self.error = error + + +class ErrorResponseException(HttpOperationError): + """Server responsed with exception of type: 'ErrorResponse'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) diff --git a/azext_iot/sdk/digitaltwins/models/event_route.py b/azext_iot/sdk/digitaltwins/models/event_route.py new file mode 100644 index 000000000..f98c05c2c --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/event_route.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoute(Model): + """A route which directs notification and telemetry events to an endpoint. + Endpoints are a destination outside of Azure Digital Twins such as an + EventHub. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: The id of the event route. + :vartype id: str + :param endpoint_id: Required. The id of the endpoint this event route is + bound to. + :type endpoint_id: str + :param filter: An expression which describes the events which are routed + to the endpoint. + :type filter: str + """ + + # @digimaun - endpointId/endpoint_id is now endpointName/endpoint_name + _validation = { + 'id': {'readonly': True}, + 'endpoint_name': {'required': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'endpoint_name': {'key': 'endpointName', 'type': 'str'}, + 'filter': {'key': 'filter', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(EventRoute, self).__init__(**kwargs) + self.id = None + self.endpoint_name = kwargs.get('endpoint_name', None) + self.filter = kwargs.get('filter', None) diff --git a/azext_iot/sdk/digitaltwins/models/event_route_paged.py b/azext_iot/sdk/digitaltwins/models/event_route_paged.py new file mode 100644 index 000000000..7b1ce3ea3 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/event_route_paged.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.paging import Paged + + +class EventRoutePaged(Paged): + """ + A paging container for iterating over a list of :class:`EventRoute ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[EventRoute]'} + } + + def __init__(self, *args, **kwargs): + + super(EventRoutePaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins/models/event_route_py3.py b/azext_iot/sdk/digitaltwins/models/event_route_py3.py new file mode 100644 index 000000000..b3e3a58c6 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/event_route_py3.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoute(Model): + """A route which directs notification and telemetry events to an endpoint. + Endpoints are a destination outside of Azure Digital Twins such as an + EventHub. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: The id of the event route. + :vartype id: str + :param endpoint_id: Required. The id of the endpoint this event route is + bound to. + :type endpoint_id: str + :param filter: An expression which describes the events which are routed + to the endpoint. + :type filter: str + """ + + # @digimaun - endpointId/endpoint_id is now endpointName/endpoint_name + _validation = { + 'id': {'readonly': True}, + 'endpoint_name': {'required': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'endpoint_name': {'key': 'endpointName', 'type': 'str'}, + 'filter': {'key': 'filter', 'type': 'str'}, + } + + def __init__(self, *, endpoint_name: str, filter: str=None, **kwargs) -> None: + super(EventRoute, self).__init__(**kwargs) + self.id = None + self.endpoint_name = endpoint_name + self.filter = filter diff --git a/azext_iot/sdk/digitaltwins/models/event_routes_list_options.py b/azext_iot/sdk/digitaltwins/models/event_routes_list_options.py new file mode 100644 index 000000000..ae16d6034 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/event_routes_list_options.py @@ -0,0 +1,30 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoutesListOptions(Model): + """Additional parameters for list operation. + + :param max_item_count: The maximum number of items to retrieve per + request. The server may choose to return less than the requested max. + Default value: -1 . + :type max_item_count: int + """ + + _attribute_map = { + 'max_item_count': {'key': '', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(EventRoutesListOptions, self).__init__(**kwargs) + self.max_item_count = kwargs.get('max_item_count', -1) diff --git a/azext_iot/sdk/digitaltwins/models/event_routes_list_options_py3.py b/azext_iot/sdk/digitaltwins/models/event_routes_list_options_py3.py new file mode 100644 index 000000000..9e17db02b --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/event_routes_list_options_py3.py @@ -0,0 +1,30 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoutesListOptions(Model): + """Additional parameters for list operation. + + :param max_item_count: The maximum number of items to retrieve per + request. The server may choose to return less than the requested max. + Default value: -1 . + :type max_item_count: int + """ + + _attribute_map = { + 'max_item_count': {'key': '', 'type': 'int'}, + } + + def __init__(self, *, max_item_count: int=-1, **kwargs) -> None: + super(EventRoutesListOptions, self).__init__(**kwargs) + self.max_item_count = max_item_count diff --git a/azext_iot/sdk/digitaltwins/models/incoming_relationship.py b/azext_iot/sdk/digitaltwins/models/incoming_relationship.py new file mode 100644 index 000000000..25d97313c --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/incoming_relationship.py @@ -0,0 +1,43 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IncomingRelationship(Model): + """An incoming relationship. + + :param relationship_id: A user-provided string representing the id of this + relationship, unique in the context of the source digital twin, i.e. + sourceId + relationshipId is unique in the context of the service. + :type relationship_id: str + :param source_id: The id of the source digital twin. + :type source_id: str + :param relationship_name: The name of the relationship. + :type relationship_name: str + :param relationship_link: Link to the relationship, to be used for + deletion. + :type relationship_link: str + """ + + _attribute_map = { + 'relationship_id': {'key': '$relationshipId', 'type': 'str'}, + 'source_id': {'key': '$sourceId', 'type': 'str'}, + 'relationship_name': {'key': '$relationshipName', 'type': 'str'}, + 'relationship_link': {'key': '$relationshipLink', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(IncomingRelationship, self).__init__(**kwargs) + self.relationship_id = kwargs.get('relationship_id', None) + self.source_id = kwargs.get('source_id', None) + self.relationship_name = kwargs.get('relationship_name', None) + self.relationship_link = kwargs.get('relationship_link', None) diff --git a/azext_iot/sdk/digitaltwins/models/incoming_relationship_paged.py b/azext_iot/sdk/digitaltwins/models/incoming_relationship_paged.py new file mode 100644 index 000000000..d51ad0e33 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/incoming_relationship_paged.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.paging import Paged + + +class IncomingRelationshipPaged(Paged): + """ + A paging container for iterating over a list of :class:`IncomingRelationship ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[IncomingRelationship]'} + } + + def __init__(self, *args, **kwargs): + + super(IncomingRelationshipPaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins/models/incoming_relationship_py3.py b/azext_iot/sdk/digitaltwins/models/incoming_relationship_py3.py new file mode 100644 index 000000000..6bb774195 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/incoming_relationship_py3.py @@ -0,0 +1,43 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IncomingRelationship(Model): + """An incoming relationship. + + :param relationship_id: A user-provided string representing the id of this + relationship, unique in the context of the source digital twin, i.e. + sourceId + relationshipId is unique in the context of the service. + :type relationship_id: str + :param source_id: The id of the source digital twin. + :type source_id: str + :param relationship_name: The name of the relationship. + :type relationship_name: str + :param relationship_link: Link to the relationship, to be used for + deletion. + :type relationship_link: str + """ + + _attribute_map = { + 'relationship_id': {'key': '$relationshipId', 'type': 'str'}, + 'source_id': {'key': '$sourceId', 'type': 'str'}, + 'relationship_name': {'key': '$relationshipName', 'type': 'str'}, + 'relationship_link': {'key': '$relationshipLink', 'type': 'str'}, + } + + def __init__(self, *, relationship_id: str=None, source_id: str=None, relationship_name: str=None, relationship_link: str=None, **kwargs) -> None: + super(IncomingRelationship, self).__init__(**kwargs) + self.relationship_id = relationship_id + self.source_id = source_id + self.relationship_name = relationship_name + self.relationship_link = relationship_link diff --git a/azext_iot/sdk/digitaltwins/models/inner_error.py b/azext_iot/sdk/digitaltwins/models/inner_error.py new file mode 100644 index 000000000..36204f33a --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/inner_error.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class InnerError(Model): + """A more specific error description than was provided by the containing + error. + + :param code: A more specific error code than was provided by the + containing error. + :type code: str + :param innererror: An object containing more specific information than the + current object about the error. + :type innererror: ~digitaltwins.models.InnerError + """ + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'innererror': {'key': 'innererror', 'type': 'InnerError'}, + } + + def __init__(self, **kwargs): + super(InnerError, self).__init__(**kwargs) + self.code = kwargs.get('code', None) + self.innererror = kwargs.get('innererror', None) diff --git a/azext_iot/sdk/digitaltwins/models/inner_error_py3.py b/azext_iot/sdk/digitaltwins/models/inner_error_py3.py new file mode 100644 index 000000000..46071b562 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/inner_error_py3.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class InnerError(Model): + """A more specific error description than was provided by the containing + error. + + :param code: A more specific error code than was provided by the + containing error. + :type code: str + :param innererror: An object containing more specific information than the + current object about the error. + :type innererror: ~digitaltwins.models.InnerError + """ + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'innererror': {'key': 'innererror', 'type': 'InnerError'}, + } + + def __init__(self, *, code: str=None, innererror=None, **kwargs) -> None: + super(InnerError, self).__init__(**kwargs) + self.code = code + self.innererror = innererror diff --git a/azext_iot/sdk/digitaltwins/models/model_data.py b/azext_iot/sdk/digitaltwins/models/model_data.py new file mode 100644 index 000000000..db3fa699c --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/model_data.py @@ -0,0 +1,59 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelData(Model): + """A model definition and metadata for that model. + + All required parameters must be populated in order to send to Azure. + + :param display_name: A language map that contains the localized display + names as specified in the model definition. + :type display_name: object + :param description: A language map that contains the localized + descriptions as specified in the model definition. + :type description: object + :param id: Required. The id of the model as specified in the model + definition. + :type id: str + :param upload_time: The time the model was uploaded to the service. + :type upload_time: datetime + :param decommissioned: Indicates if the model is decommissioned. + Decommissioned models cannot be referenced by newly created digital twins. + Default value: False . + :type decommissioned: bool + :param model: The model definition. + :type model: object + """ + + _validation = { + 'id': {'required': True}, + } + + _attribute_map = { + 'display_name': {'key': 'displayName', 'type': 'object'}, + 'description': {'key': 'description', 'type': 'object'}, + 'id': {'key': 'id', 'type': 'str'}, + 'upload_time': {'key': 'uploadTime', 'type': 'iso-8601'}, + 'decommissioned': {'key': 'decommissioned', 'type': 'bool'}, + 'model': {'key': 'model', 'type': 'object'}, + } + + def __init__(self, **kwargs): + super(ModelData, self).__init__(**kwargs) + self.display_name = kwargs.get('display_name', None) + self.description = kwargs.get('description', None) + self.id = kwargs.get('id', None) + self.upload_time = kwargs.get('upload_time', None) + self.decommissioned = kwargs.get('decommissioned', False) + self.model = kwargs.get('model', None) diff --git a/azext_iot/sdk/digitaltwins/models/model_data_paged.py b/azext_iot/sdk/digitaltwins/models/model_data_paged.py new file mode 100644 index 000000000..b36b45be4 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/model_data_paged.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.paging import Paged + + +class ModelDataPaged(Paged): + """ + A paging container for iterating over a list of :class:`ModelData ` object + """ + + # @digimaun - [ModelData] -> [object] + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[object]'} + } + + def __init__(self, *args, **kwargs): + + super(ModelDataPaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins/models/model_data_py3.py b/azext_iot/sdk/digitaltwins/models/model_data_py3.py new file mode 100644 index 000000000..667e2fb2e --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/model_data_py3.py @@ -0,0 +1,59 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelData(Model): + """A model definition and metadata for that model. + + All required parameters must be populated in order to send to Azure. + + :param display_name: A language map that contains the localized display + names as specified in the model definition. + :type display_name: object + :param description: A language map that contains the localized + descriptions as specified in the model definition. + :type description: object + :param id: Required. The id of the model as specified in the model + definition. + :type id: str + :param upload_time: The time the model was uploaded to the service. + :type upload_time: datetime + :param decommissioned: Indicates if the model is decommissioned. + Decommissioned models cannot be referenced by newly created digital twins. + Default value: False . + :type decommissioned: bool + :param model: The model definition. + :type model: object + """ + + _validation = { + 'id': {'required': True}, + } + + _attribute_map = { + 'display_name': {'key': 'displayName', 'type': 'object'}, + 'description': {'key': 'description', 'type': 'object'}, + 'id': {'key': 'id', 'type': 'str'}, + 'upload_time': {'key': 'uploadTime', 'type': 'iso-8601'}, + 'decommissioned': {'key': 'decommissioned', 'type': 'bool'}, + 'model': {'key': 'model', 'type': 'object'}, + } + + def __init__(self, *, id: str, display_name=None, description=None, upload_time=None, decommissioned: bool=False, model=None, **kwargs) -> None: + super(ModelData, self).__init__(**kwargs) + self.display_name = display_name + self.description = description + self.id = id + self.upload_time = upload_time + self.decommissioned = decommissioned + self.model = model diff --git a/azext_iot/sdk/digitaltwins/models/object_paged.py b/azext_iot/sdk/digitaltwins/models/object_paged.py new file mode 100644 index 000000000..ebed1de3a --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/object_paged.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.paging import Paged + + +class ObjectPaged(Paged): + """ + A paging container for iterating over a list of object object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[object]'} + } + + def __init__(self, *args, **kwargs): + + super(ObjectPaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins/models/query_result.py b/azext_iot/sdk/digitaltwins/models/query_result.py new file mode 100644 index 000000000..7349e2290 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/query_result.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class QueryResult(Model): + """The results of a query operation and an optional continuation token. + + :param items: The query results. + :type items: list[object] + :param continuation_token: A token which can be used to construct a new + QuerySpecification to retrieve the next set of results. + :type continuation_token: str + """ + + _attribute_map = { + 'items': {'key': 'items', 'type': '[object]'}, + 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(QueryResult, self).__init__(**kwargs) + self.items = kwargs.get('items', None) + self.continuation_token = kwargs.get('continuation_token', None) diff --git a/azext_iot/sdk/digitaltwins/models/query_result_py3.py b/azext_iot/sdk/digitaltwins/models/query_result_py3.py new file mode 100644 index 000000000..ef38eb6d8 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/query_result_py3.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class QueryResult(Model): + """The results of a query operation and an optional continuation token. + + :param items: The query results. + :type items: list[object] + :param continuation_token: A token which can be used to construct a new + QuerySpecification to retrieve the next set of results. + :type continuation_token: str + """ + + _attribute_map = { + 'items': {'key': 'items', 'type': '[object]'}, + 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, + } + + def __init__(self, *, items=None, continuation_token: str=None, **kwargs) -> None: + super(QueryResult, self).__init__(**kwargs) + self.items = items + self.continuation_token = continuation_token diff --git a/azext_iot/sdk/digitaltwins/models/query_specification.py b/azext_iot/sdk/digitaltwins/models/query_specification.py new file mode 100644 index 000000000..854db4323 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/query_specification.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class QuerySpecification(Model): + """A query specification containing either a query statement or a continuation + token from a previous query result. + + :param query: The query to execute. This value is ignored if a + continuation token is provided. + :type query: str + :param continuation_token: A token which is used to retrieve the next set + of results from a previous query. + :type continuation_token: str + """ + + _attribute_map = { + 'query': {'key': 'query', 'type': 'str'}, + 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(QuerySpecification, self).__init__(**kwargs) + self.query = kwargs.get('query', None) + self.continuation_token = kwargs.get('continuation_token', None) diff --git a/azext_iot/sdk/digitaltwins/models/query_specification_py3.py b/azext_iot/sdk/digitaltwins/models/query_specification_py3.py new file mode 100644 index 000000000..7aa1b5449 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/models/query_specification_py3.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class QuerySpecification(Model): + """A query specification containing either a query statement or a continuation + token from a previous query result. + + :param query: The query to execute. This value is ignored if a + continuation token is provided. + :type query: str + :param continuation_token: A token which is used to retrieve the next set + of results from a previous query. + :type continuation_token: str + """ + + _attribute_map = { + 'query': {'key': 'query', 'type': 'str'}, + 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, + } + + def __init__(self, *, query: str=None, continuation_token: str=None, **kwargs) -> None: + super(QuerySpecification, self).__init__(**kwargs) + self.query = query + self.continuation_token = continuation_token diff --git a/azext_iot/sdk/digitaltwins/operations/__init__.py b/azext_iot/sdk/digitaltwins/operations/__init__.py new file mode 100644 index 000000000..80eca0cbf --- /dev/null +++ b/azext_iot/sdk/digitaltwins/operations/__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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twin_models_operations import DigitalTwinModelsOperations +from .query_operations import QueryOperations +from .digital_twins_operations import DigitalTwinsOperations +from .event_routes_operations import EventRoutesOperations + +__all__ = [ + 'DigitalTwinModelsOperations', + 'QueryOperations', + 'DigitalTwinsOperations', + 'EventRoutesOperations', +] diff --git a/azext_iot/sdk/digitaltwins/operations/digital_twin_models_operations.py b/azext_iot/sdk/digitaltwins/operations/digital_twin_models_operations.py new file mode 100644 index 000000000..7c39155cc --- /dev/null +++ b/azext_iot/sdk/digitaltwins/operations/digital_twin_models_operations.py @@ -0,0 +1,363 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class DigitalTwinModelsOperations(object): + """DigitalTwinModelsOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The requested API version. Constant value: "2020-05-31-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-05-31-preview" + + self.config = config + + def add( + self, models=None, custom_headers=None, raw=False, **operation_config): + """Uploads one or more models. When any error occurs, no models are + uploaded. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 409 (Conflict): One or more of the provided models already exist. + + :param models: An array of models to add. + :type models: list[object] + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: list or ClientRawResponse if raw=true + :rtype: list[~digitaltwins.models.ModelData] or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.add.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if models is not None: + body_content = self._serialize.body(models, '[object]') + else: + body_content = None + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + # @digimaun - custom response handling + return response + add.metadata = {'url': '/models'} + + def list( + self, dependencies_for=None, include_model_definition=False, digital_twin_models_list_options=None, custom_headers=None, raw=False, **operation_config): + """Retrieves model metadata and, optionally, model definitions. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + + :param dependencies_for: The set of the models which will have their + dependencies retrieved. If omitted, all models are retrieved. + :type dependencies_for: list[str] + :param include_model_definition: When true the model definition will + be returned as part of the result. + :type include_model_definition: bool + :param digital_twin_models_list_options: Additional parameters for the + operation + :type digital_twin_models_list_options: + ~digitaltwins.models.DigitalTwinModelsListOptions + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of ModelData + :rtype: + ~digitaltwins.models.ModelDataPaged[~digitaltwins.models.ModelData] + :raises: + :class:`ErrorResponseException` + """ + max_item_count = None + if digital_twin_models_list_options is not None: + max_item_count = digital_twin_models_list_options.max_item_count + + def internal_paging(next_link=None, raw=False): + + if not next_link: + # Construct URL + url = self.list.metadata['url'] + + # Construct parameters + query_parameters = {} + if dependencies_for is not None: + # @digimaun - super hackery to support collectionFormat: multi + concatted_qs = "" + for dtmi in dependencies_for: + concatted_qs = concatted_qs + self._serialize.query("dependencies_for", dtmi, 'str') + if dtmi != dependencies_for[-1]: + concatted_qs = concatted_qs + "&dependenciesFor=" + + query_parameters['dependenciesFor'] = concatted_qs + + if include_model_definition is not None: + query_parameters['includeModelDefinition'] = self._serialize.query("include_model_definition", include_model_definition, 'bool') + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if max_item_count is not None: + header_parameters['x-ms-max-item-count'] = self._serialize.header("max_item_count", max_item_count, 'int') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + deserialized = models.ModelDataPaged(internal_paging, self._deserialize.dependencies) + + if raw: + header_dict = {} + client_raw_response = models.ModelDataPaged(internal_paging, self._deserialize.dependencies, header_dict) + return client_raw_response + + return deserialized + list.metadata = {'url': '/models'} + + def get_by_id( + self, id, include_model_definition=False, custom_headers=None, raw=False, **operation_config): + """Retrieves model metadata and optionally the model definition. + Status codes: + 200 (OK): Success. + 404 (Not Found): There is no model with the provided id. + + :param id: The id for the model. The id is globally unique and case + sensitive. + :type id: str + :param include_model_definition: When true the model definition will + be returned as part of the result. + :type include_model_definition: bool + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: ModelData or ClientRawResponse if raw=true + :rtype: ~digitaltwins.models.ModelData or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get_by_id.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + if include_model_definition is not None: + query_parameters['includeModelDefinition'] = self._serialize.query("include_model_definition", include_model_definition, 'bool') + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('ModelData', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_by_id.metadata = {'url': '/models/{id}'} + + def update( + self, id, update_model, custom_headers=None, raw=False, **operation_config): + """Updates the metadata for a model. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is no model with the provided id. + + :param id: The id for the model. The id is globally unique and case + sensitive. + :type id: str + :param update_model: An update specification described by JSON Patch. + Only the decommissioned property can be replaced. + :type update_model: list[object] + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.update.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(update_model, '[object]') + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + update.metadata = {'url': '/models/{id}'} + + def delete( + self, id, custom_headers=None, raw=False, **operation_config): + """Deletes a model. A model can only be deleted if no other models + reference it. + Status codes: + 204 (No Content): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is no model with the provided id. + 409 (Conflict): There are dependencies on the model that prevent it + from being deleted. + + :param id: The id for the model. The id is globally unique and case + sensitive. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.delete.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete.metadata = {'url': '/models/{id}'} diff --git a/azext_iot/sdk/digitaltwins/operations/digital_twins_operations.py b/azext_iot/sdk/digitaltwins/operations/digital_twins_operations.py new file mode 100644 index 000000000..e6a205893 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/operations/digital_twins_operations.py @@ -0,0 +1,1037 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class DigitalTwinsOperations(object): + """DigitalTwinsOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The requested API version. Constant value: "2020-05-31-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-05-31-preview" + + self.config = config + + def get_by_id( + self, id, custom_headers=None, raw=False, **operation_config): + """Retrieves a digital twin. + Status codes: + 200 (OK): Success. + 404 (Not Found): There is no digital twin with the provided id. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get_by_id.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'ETag': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + get_by_id.metadata = {'url': '/digitaltwins/{id}'} + + def add( + self, id, twin, if_none_match=None, custom_headers=None, raw=False, **operation_config): + """Adds or replaces a digital twin. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 412 (Precondition Failed): The model is decommissioned or the digital + twin already exists (when using If-None-Match: *). + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param twin: The digital twin instance being added. If provided, the + $dtId property is ignored. + :type twin: object + :param if_none_match: Only perform the operation if the entity does + not already exist. Possible values include: '*' + :type if_none_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.add.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_none_match is not None: + header_parameters['If-None-Match'] = self._serialize.header("if_none_match", if_none_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(twin, 'object') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 202]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'ETag': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + add.metadata = {'url': '/digitaltwins/{id}'} + + def delete( + self, id, if_match=None, custom_headers=None, raw=False, **operation_config): + """Deletes a digital twin. All relationships referencing the digital twin + must already be deleted. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is no digital twin with the provided id. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param if_match: Only perform the operation if the entity's etag + matches one of the etags provided or * is provided. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.delete.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete.metadata = {'url': '/digitaltwins/{id}'} + + def update( + self, id, patch_document, if_match=None, custom_headers=None, raw=False, **operation_config): + """Updates a digital twin. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is no digital twin with the provided id. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param patch_document: An update specification described by JSON + Patch. Updates to property values and $model elements may happen in + the same request. Operations are limited to add, replace and remove. + :type patch_document: list[object] + :param if_match: Only perform the operation if the entity's etag + matches one of the etags provided or * is provided. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.update.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(patch_document, '[object]') + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [202, 204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + client_raw_response.add_headers({ + 'ETag': 'str', + }) + return client_raw_response + update.metadata = {'url': '/digitaltwins/{id}'} + + def get_relationship_by_id( + self, id, relationship_id, custom_headers=None, raw=False, **operation_config): + """Retrieves a relationship between two digital twins. + Status codes: + 200 (OK): Success. + 404 (Not Found): There is either no digital twin or relationship with + the provided id. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param relationship_id: The id of the relationship. The id is unique + within the digital twin and case sensitive. + :type relationship_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get_relationship_by_id.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'relationshipId': self._serialize.url("relationship_id", relationship_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'ETag': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + get_relationship_by_id.metadata = {'url': '/digitaltwins/{id}/relationships/{relationshipId}'} + + def add_relationship( + self, id, relationship_id, relationship=None, if_none_match=None, custom_headers=None, raw=False, **operation_config): + """Adds a relationship between two digital twins. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is either no digital twin, target digital twin, + or relationship with the provided id. + 409 (Conflict): A relationship with the provided id already exists. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param relationship_id: The id of the relationship. The id is unique + within the digital twin and case sensitive. + :type relationship_id: str + :param relationship: The data for the relationship. + :type relationship: object + :param if_none_match: Only perform the operation if the entity does + not already exist. Possible values include: '*' + :type if_none_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.add_relationship.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'relationshipId': self._serialize.url("relationship_id", relationship_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_none_match is not None: + header_parameters['If-None-Match'] = self._serialize.header("if_none_match", if_none_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if relationship is not None: + body_content = self._serialize.body(relationship, 'object') + else: + body_content = None + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'ETag': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + add_relationship.metadata = {'url': '/digitaltwins/{id}/relationships/{relationshipId}'} + + def delete_relationship( + self, id, relationship_id, if_match=None, custom_headers=None, raw=False, **operation_config): + """Deletes a relationship between two digital twins. + Status codes: + 200 (OK): Success. + 404 (Not Found): There is either no digital twin or relationship with + the provided id. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param relationship_id: The id of the relationship. The id is unique + within the digital twin and case sensitive. + :type relationship_id: str + :param if_match: Only perform the operation if the entity's etag + matches one of the etags provided or * is provided. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.delete_relationship.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'relationshipId': self._serialize.url("relationship_id", relationship_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete_relationship.metadata = {'url': '/digitaltwins/{id}/relationships/{relationshipId}'} + + def update_relationship( + self, id, relationship_id, patch_document=None, if_match=None, custom_headers=None, raw=False, **operation_config): + """Updates the properties on a relationship between two digital twins. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is either no digital twin or relationship with + the provided id. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param relationship_id: The id of the relationship. The id is unique + within the digital twin and case sensitive. + :type relationship_id: str + :param patch_document: JSON Patch description of the update to the + relationship properties. + :type patch_document: list[object] + :param if_match: Only perform the operation if the entity's etag + matches one of the etags provided or * is provided. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.update_relationship.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'relationshipId': self._serialize.url("relationship_id", relationship_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if patch_document is not None: + body_content = self._serialize.body(patch_document, '[object]') + else: + body_content = None + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + client_raw_response.add_headers({ + 'ETag': 'str', + }) + return client_raw_response + update_relationship.metadata = {'url': '/digitaltwins/{id}/relationships/{relationshipId}'} + + def list_relationships( + self, id, relationship_name=None, custom_headers=None, raw=False, **operation_config): + """Retrieves the relationships from a digital twin. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is no digital twin with the provided id. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param relationship_name: The name of the relationship. + :type relationship_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of object + :rtype: ~digitaltwins.models.ObjectPaged[object] + :raises: + :class:`ErrorResponseException` + """ + def internal_paging(next_link=None, raw=False): + + if not next_link: + # Construct URL + url = self.list_relationships.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + if relationship_name is not None: + query_parameters['relationshipName'] = self._serialize.query("relationship_name", relationship_name, 'str') + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + deserialized = models.ObjectPaged(internal_paging, self._deserialize.dependencies) + + if raw: + header_dict = {} + client_raw_response = models.ObjectPaged(internal_paging, self._deserialize.dependencies, header_dict) + return client_raw_response + + return deserialized + list_relationships.metadata = {'url': '/digitaltwins/{id}/relationships'} + + def list_incoming_relationships( + self, id, custom_headers=None, raw=False, **operation_config): + """Retrieves all incoming relationship for a digital twin. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is no digital twin with the provided id. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of IncomingRelationship + :rtype: + ~digitaltwins.models.IncomingRelationshipPaged[~digitaltwins.models.IncomingRelationship] + :raises: + :class:`ErrorResponseException` + """ + def internal_paging(next_link=None, raw=False): + + if not next_link: + # Construct URL + url = self.list_incoming_relationships.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + deserialized = models.IncomingRelationshipPaged(internal_paging, self._deserialize.dependencies) + + if raw: + header_dict = {} + client_raw_response = models.IncomingRelationshipPaged(internal_paging, self._deserialize.dependencies, header_dict) + return client_raw_response + + return deserialized + list_incoming_relationships.metadata = {'url': '/digitaltwins/{id}/incomingrelationships'} + + def send_telemetry( + self, id, telemetry, dt_id, dt_timestamp=None, custom_headers=None, raw=False, **operation_config): + """Sends telemetry on behalf of a digital twin. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is no digital twin with the provided id. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param telemetry: The telemetry measurements to send from the digital + twin. + :type telemetry: object + :param dt_id: A unique message identifier (in the scope of the digital + twin id) that is commonly used for de-duplicating messages. + :type dt_id: str + :param dt_timestamp: An RFC 3339 timestamp that identifies the time + the telemetry was measured. + :type dt_timestamp: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.send_telemetry.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + + # @digimaun - add api-version query parameter + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + header_parameters['dt-id'] = self._serialize.header("dt_id", dt_id, 'str') + if dt_timestamp is not None: + header_parameters['dt-timestamp'] = self._serialize.header("dt_timestamp", dt_timestamp, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(telemetry, 'object') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + send_telemetry.metadata = {'url': '/digitaltwins/{id}/telemetry'} + + def send_component_telemetry( + self, id, component_path, telemetry, dt_id, dt_timestamp=None, custom_headers=None, raw=False, **operation_config): + """Sends telemetry on behalf of a component in a digital twin. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is either no digital twin with the provided id + or the component path is invalid. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param component_path: The name of the DTDL component. + :type component_path: str + :param telemetry: The telemetry measurements to send from the digital + twin's component. + :type telemetry: object + :param dt_id: A unique message identifier (in the scope of the digital + twin id) that is commonly used for de-duplicating messages. + :type dt_id: str + :param dt_timestamp: An RFC 3339 timestamp that identifies the time + the telemetry was measured. + :type dt_timestamp: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.send_component_telemetry.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'componentPath': self._serialize.url("component_path", component_path, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + + # @digimaun - add api-version query parameter + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + header_parameters['dt-id'] = self._serialize.header("dt_id", dt_id, 'str') + if dt_timestamp is not None: + header_parameters['dt-timestamp'] = self._serialize.header("dt_timestamp", dt_timestamp, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(telemetry, 'object') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + send_component_telemetry.metadata = {'url': '/digitaltwins/{id}/components/{componentPath}/telemetry'} + + def get_component( + self, id, component_path, custom_headers=None, raw=False, **operation_config): + """Retrieves a component from a digital twin. + Status codes: + 200 (OK): Success. + 404 (Not Found): There is either no digital twin with the provided id + or the component path is invalid. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param component_path: The name of the DTDL component. + :type component_path: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get_component.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'componentPath': self._serialize.url("component_path", component_path, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'ETag': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + get_component.metadata = {'url': '/digitaltwins/{id}/components/{componentPath}'} + + def update_component( + self, id, component_path, patch_document=None, if_match=None, custom_headers=None, raw=False, **operation_config): + """Updates a component on a digital twin. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + 404 (Not Found): There is either no digital twin with the provided id + or the component path is invalid. + + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param component_path: The name of the DTDL component. + :type component_path: str + :param patch_document: An update specification described by JSON + Patch. Updates to property values and $model elements may happen in + the same request. Operations are limited to add, replace and remove. + :type patch_document: list[object] + :param if_match: Only perform the operation if the entity's etag + matches one of the etags provided or * is provided. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.update_component.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'componentPath': self._serialize.url("component_path", component_path, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if patch_document is not None: + body_content = self._serialize.body(patch_document, '[object]') + else: + body_content = None + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [202, 204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + client_raw_response.add_headers({ + 'ETag': 'str', + }) + return client_raw_response + update_component.metadata = {'url': '/digitaltwins/{id}/components/{componentPath}'} diff --git a/azext_iot/sdk/digitaltwins/operations/event_routes_operations.py b/azext_iot/sdk/digitaltwins/operations/event_routes_operations.py new file mode 100644 index 000000000..2969ce3a1 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/operations/event_routes_operations.py @@ -0,0 +1,292 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class EventRoutesOperations(object): + """EventRoutesOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The requested API version. Constant value: "2020-05-31-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-05-31-preview" + + self.config = config + + def list( + self, event_routes_list_options=None, custom_headers=None, raw=False, **operation_config): + """Retrieves all event routes. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + + :param event_routes_list_options: Additional parameters for the + operation + :type event_routes_list_options: + ~digitaltwins.models.EventRoutesListOptions + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of EventRoute + :rtype: + ~digitaltwins.models.EventRoutePaged[~digitaltwins.models.EventRoute] + :raises: + :class:`ErrorResponseException` + """ + max_item_count = None + if event_routes_list_options is not None: + max_item_count = event_routes_list_options.max_item_count + + def internal_paging(next_link=None, raw=False): + + if not next_link: + # Construct URL + url = self.list.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if max_item_count is not None: + header_parameters['x-ms-max-item-count'] = self._serialize.header("max_item_count", max_item_count, 'int') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + deserialized = models.EventRoutePaged(internal_paging, self._deserialize.dependencies) + + if raw: + header_dict = {} + client_raw_response = models.EventRoutePaged(internal_paging, self._deserialize.dependencies, header_dict) + return client_raw_response + + return deserialized + list.metadata = {'url': '/eventroutes'} + + def get_by_id( + self, id, custom_headers=None, raw=False, **operation_config): + """Retrieves an event route. + Status codes: + 200 (OK): Success. + 404 (Not Found): There is no event route with the provided id. + + :param id: The id for an event route. The id is unique within event + routes and case sensitive. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: EventRoute or ClientRawResponse if raw=true + :rtype: ~digitaltwins.models.EventRoute or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get_by_id.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('EventRoute', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_by_id.metadata = {'url': '/eventroutes/{id}'} + + def add( + self, id, endpoint_id, filter=None, custom_headers=None, raw=False, **operation_config): + """Adds or replaces an event route. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + + :param id: The id for an event route. The id is unique within event + routes and case sensitive. + :type id: str + :param endpoint_id: The id of the endpoint this event route is bound + to. + :type endpoint_id: str + :param filter: An expression which describes the events which are + routed to the endpoint. + :type filter: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + event_route = None + if endpoint_id is not None or filter is not None: + # @digimaun - custom change related to endpointId -> endpointName + event_route = models.EventRoute(endpoint_name=endpoint_id, filter=filter) + + # Construct URL + url = self.add.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if event_route is not None: + body_content = self._serialize.body(event_route, 'EventRoute') + else: + body_content = None + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + add.metadata = {'url': '/eventroutes/{id}'} + + def delete( + self, id, custom_headers=None, raw=False, **operation_config): + """Deletes an event route. + Status codes: + 200 (OK): Success. + 404 (Not Found): There is no event route with the provided id. + + :param id: The id for an event route. The id is unique within event + routes and case sensitive. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.delete.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete.metadata = {'url': '/eventroutes/{id}'} diff --git a/azext_iot/sdk/digitaltwins/operations/query_operations.py b/azext_iot/sdk/digitaltwins/operations/query_operations.py new file mode 100644 index 000000000..9c89d5f62 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/operations/query_operations.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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class QueryOperations(object): + """QueryOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The requested API version. Constant value: "2020-05-31-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-05-31-preview" + + self.config = config + + def query_twins( + self, query=None, continuation_token=None, custom_headers=None, raw=False, **operation_config): + """Executes a query that allows traversing relationships and filtering by + property values. + Status codes: + 200 (OK): Success. + 400 (Bad Request): The request is invalid. + + :param query: The query to execute. This value is ignored if a + continuation token is provided. + :type query: str + :param continuation_token: A token which is used to retrieve the next + set of results from a previous query. + :type continuation_token: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: QueryResult or ClientRawResponse if raw=true + :rtype: ~digitaltwins.models.QueryResult or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + query_specification = models.QuerySpecification(query=query, continuation_token=continuation_token) + + # Construct URL + url = self.query_twins.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(query_specification, 'QuerySpecification') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('QueryResult', response) + header_dict = { + 'query-charge': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + query_twins.metadata = {'url': '/query'} diff --git a/azext_iot/sdk/digitaltwins/version.py b/azext_iot/sdk/digitaltwins/version.py new file mode 100644 index 000000000..aa93dc9e0 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/version.py @@ -0,0 +1,13 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +VERSION = "2020-05-31-preview" + diff --git a/azext_iot/sdk/digitaltwins_arm/__init__.py b/azext_iot/sdk/digitaltwins_arm/__init__.py new file mode 100644 index 000000000..3d6c9db37 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .azure_digital_twins_management_client import AzureDigitalTwinsManagementClient +from .version import VERSION + +__all__ = ['AzureDigitalTwinsManagementClient'] + +__version__ = VERSION + diff --git a/azext_iot/sdk/digitaltwins_arm/azure_digital_twins_management_client.py b/azext_iot/sdk/digitaltwins_arm/azure_digital_twins_management_client.py new file mode 100644 index 000000000..bf2c85f4a --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/azure_digital_twins_management_client.py @@ -0,0 +1,99 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.service_client import SDKClient +from msrest import Serializer, Deserializer +from msrestazure import AzureConfiguration +from .version import VERSION +from .operations.digital_twins_operations import DigitalTwinsOperations +from .operations.digital_twins_endpoint_operations import DigitalTwinsEndpointOperations +from .operations.operations import Operations +from . import models +from azext_iot.constants import USER_AGENT + + +class AzureDigitalTwinsManagementClientConfiguration(AzureConfiguration): + """Configuration for AzureDigitalTwinsManagementClient + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param subscription_id: The subscription identifier. + :type subscription_id: str + :param str base_url: Service URL + """ + + def __init__( + self, credentials, subscription_id, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if subscription_id is None: + raise ValueError("Parameter 'subscription_id' must not be None.") + if not base_url: + base_url = 'https://management.azure.com' + + # @digimaun - use azmock server + # base_url = 'http://127.0.0.1:8005' + + super(AzureDigitalTwinsManagementClientConfiguration, self).__init__(base_url) + + self.add_user_agent('azuredigitaltwinsmanagementclient/{}'.format(VERSION)) + self.add_user_agent(USER_AGENT) + + self.credentials = credentials + self.subscription_id = subscription_id + + +class AzureDigitalTwinsManagementClient(SDKClient): + """Azure Digital Twins Client for managing DigitalTwinsInstance + + :ivar config: Configuration for client. + :vartype config: AzureDigitalTwinsManagementClientConfiguration + + :ivar digital_twins: DigitalTwins operations + :vartype digital_twins: digitaltwins-arm.operations.DigitalTwinsOperations + :ivar digital_twins_endpoint: DigitalTwinsEndpoint operations + :vartype digital_twins_endpoint: digitaltwins-arm.operations.DigitalTwinsEndpointOperations + :ivar io_thub: IoTHub operations + :vartype io_thub: digitaltwins-arm.operations.IoTHubOperations + :ivar digital_twins_io_thubs: DigitalTwinsIoTHubs operations + :vartype digital_twins_io_thubs: digitaltwins-arm.operations.DigitalTwinsIoTHubsOperations + :ivar operations: Operations operations + :vartype operations: digitaltwins-arm.operations.Operations + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param subscription_id: The subscription identifier. + :type subscription_id: str + :param str base_url: Service URL + """ + + def __init__( + self, credentials, subscription_id, base_url=None): + + self.config = AzureDigitalTwinsManagementClientConfiguration(credentials, subscription_id, base_url) + super(AzureDigitalTwinsManagementClient, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2020-03-01-preview' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + self.digital_twins = DigitalTwinsOperations( + self._client, self.config, self._serialize, self._deserialize) + self.digital_twins_endpoint = DigitalTwinsEndpointOperations( + self._client, self.config, self._serialize, self._deserialize) + self.operations = Operations( + self._client, self.config, self._serialize, self._deserialize) diff --git a/azext_iot/sdk/digitaltwins_arm/models/__init__.py b/azext_iot/sdk/digitaltwins_arm/models/__init__.py new file mode 100644 index 000000000..e572c8103 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/__init__.py @@ -0,0 +1,93 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +try: + from .digital_twins_sku_info_py3 import DigitalTwinsSkuInfo + from .digital_twins_description_py3 import DigitalTwinsDescription + from .digital_twins_patch_description_py3 import DigitalTwinsPatchDescription + from .digital_twins_resource_py3 import DigitalTwinsResource + from .error_definition_py3 import ErrorDefinition + from .error_response_py3 import ErrorResponse, ErrorResponseException + from .operation_display_py3 import OperationDisplay + from .operation_py3 import Operation + from .check_name_request_py3 import CheckNameRequest + from .check_name_result_py3 import CheckNameResult + from .external_resource_py3 import ExternalResource + from .integration_resource_py3 import IntegrationResource + from .integration_resource_update_info_py3 import IntegrationResourceUpdateInfo + from .integration_resource_state1_py3 import IntegrationResourceState1 + from .digital_twins_endpoint_resource_properties_py3 import DigitalTwinsEndpointResourceProperties + from .digital_twins_endpoint_resource_py3 import DigitalTwinsEndpointResource + from .service_bus_py3 import ServiceBus + from .event_hub_py3 import EventHub + from .event_grid_py3 import EventGrid + from .digital_twins_description_list_custom import DigitalTwinsDescriptionListCustom +except (SyntaxError, ImportError): + from .digital_twins_sku_info import DigitalTwinsSkuInfo + from .digital_twins_description import DigitalTwinsDescription + from .digital_twins_patch_description import DigitalTwinsPatchDescription + from .digital_twins_resource import DigitalTwinsResource + from .error_definition import ErrorDefinition + from .error_response import ErrorResponse, ErrorResponseException + from .operation_display import OperationDisplay + from .operation import Operation + from .check_name_request import CheckNameRequest + from .check_name_result import CheckNameResult + from .external_resource import ExternalResource + from .integration_resource import IntegrationResource + from .integration_resource_update_info import IntegrationResourceUpdateInfo + from .integration_resource_state1 import IntegrationResourceState1 + from .digital_twins_endpoint_resource_properties import DigitalTwinsEndpointResourceProperties + from .digital_twins_endpoint_resource import DigitalTwinsEndpointResource + from .service_bus import ServiceBus + from .event_hub import EventHub + from .event_grid import EventGrid +from .digital_twins_description_paged import DigitalTwinsDescriptionPaged +from .digital_twins_endpoint_resource_paged import DigitalTwinsEndpointResourcePaged +from .integration_resource_paged import IntegrationResourcePaged +from .operation_paged import OperationPaged +from .azure_digital_twins_management_client_enums import ( + ProvisioningState, + Reason, + IntegrationResourceState, + EndpointProvisioningState, +) + +__all__ = [ + 'DigitalTwinsSkuInfo', + 'DigitalTwinsDescription', + 'DigitalTwinsPatchDescription', + 'DigitalTwinsResource', + 'ErrorDefinition', + 'ErrorResponse', 'ErrorResponseException', + 'OperationDisplay', + 'Operation', + 'CheckNameRequest', + 'CheckNameResult', + 'ExternalResource', + 'IntegrationResource', + 'IntegrationResourceUpdateInfo', + 'IntegrationResourceState1', + 'DigitalTwinsEndpointResourceProperties', + 'DigitalTwinsEndpointResource', + 'ServiceBus', + 'EventHub', + 'EventGrid', + 'DigitalTwinsDescriptionPaged', + 'DigitalTwinsEndpointResourcePaged', + 'IntegrationResourcePaged', + 'OperationPaged', + 'ProvisioningState', + 'Reason', + 'IntegrationResourceState', + 'EndpointProvisioningState', + 'DigitalTwinsDescriptionListCustom' +] diff --git a/azext_iot/sdk/digitaltwins_arm/models/azure_digital_twins_management_client_enums.py b/azext_iot/sdk/digitaltwins_arm/models/azure_digital_twins_management_client_enums.py new file mode 100644 index 000000000..5d4e0a7b3 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/azure_digital_twins_management_client_enums.py @@ -0,0 +1,45 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from enum import Enum + + +class ProvisioningState(str, Enum): + + provisioning = "Provisioning" + deleting = "Deleting" + succeeded = "Succeeded" + failed = "Failed" + canceled = "Canceled" + + +class Reason(str, Enum): + + invalid = "Invalid" + already_exists = "AlreadyExists" + + +class IntegrationResourceState(str, Enum): + + provisioning = "Provisioning" + deleting = "Deleting" + succeeded = "Succeeded" + failed = "Failed" + canceled = "Canceled" + + +class EndpointProvisioningState(str, Enum): + + provisioning = "Provisioning" + deleting = "Deleting" + succeeded = "Succeeded" + failed = "Failed" + canceled = "Canceled" diff --git a/azext_iot/sdk/digitaltwins_arm/models/check_name_request.py b/azext_iot/sdk/digitaltwins_arm/models/check_name_request.py new file mode 100644 index 000000000..335a3bee7 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/check_name_request.py @@ -0,0 +1,45 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CheckNameRequest(Model): + """The result returned from a database check name availability request. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param name: Required. Resource name. + :type name: str + :ivar type: Required. The type of resource, for instance + Microsoft.DigitalTwins/digitalTwinsInstances. Default value: + "Microsoft.DigitalTwins/digitalTwinsInstances" . + :vartype type: str + """ + + _validation = { + 'name': {'required': True}, + 'type': {'required': True, 'constant': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + type = "Microsoft.DigitalTwins/digitalTwinsInstances" + + def __init__(self, **kwargs): + super(CheckNameRequest, self).__init__(**kwargs) + self.name = kwargs.get('name', None) diff --git a/azext_iot/sdk/digitaltwins_arm/models/check_name_request_py3.py b/azext_iot/sdk/digitaltwins_arm/models/check_name_request_py3.py new file mode 100644 index 000000000..e75be54d7 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/check_name_request_py3.py @@ -0,0 +1,45 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CheckNameRequest(Model): + """The result returned from a database check name availability request. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param name: Required. Resource name. + :type name: str + :ivar type: Required. The type of resource, for instance + Microsoft.DigitalTwins/digitalTwinsInstances. Default value: + "Microsoft.DigitalTwins/digitalTwinsInstances" . + :vartype type: str + """ + + _validation = { + 'name': {'required': True}, + 'type': {'required': True, 'constant': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + type = "Microsoft.DigitalTwins/digitalTwinsInstances" + + def __init__(self, *, name: str, **kwargs) -> None: + super(CheckNameRequest, self).__init__(**kwargs) + self.name = name diff --git a/azext_iot/sdk/digitaltwins_arm/models/check_name_result.py b/azext_iot/sdk/digitaltwins_arm/models/check_name_result.py new file mode 100644 index 000000000..9e7a1ef67 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/check_name_result.py @@ -0,0 +1,43 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CheckNameResult(Model): + """The result returned from a check name availability request. + + :param name_available: Specifies a Boolean value that indicates if the + name is available. + :type name_available: bool + :param name: The name that was checked. + :type name: str + :param message: Message indicating an unavailable name due to a conflict, + or a description of the naming rules that are violated. + :type message: str + :param reason: Message providing the reason why the given name is invalid. + Possible values include: 'Invalid', 'AlreadyExists' + :type reason: str or ~digitaltwins-arm.models.Reason + """ + + _attribute_map = { + 'name_available': {'key': 'nameAvailable', 'type': 'bool'}, + 'name': {'key': 'name', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'reason': {'key': 'reason', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(CheckNameResult, self).__init__(**kwargs) + self.name_available = kwargs.get('name_available', None) + self.name = kwargs.get('name', None) + self.message = kwargs.get('message', None) + self.reason = kwargs.get('reason', None) diff --git a/azext_iot/sdk/digitaltwins_arm/models/check_name_result_py3.py b/azext_iot/sdk/digitaltwins_arm/models/check_name_result_py3.py new file mode 100644 index 000000000..5d38ec4f4 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/check_name_result_py3.py @@ -0,0 +1,43 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CheckNameResult(Model): + """The result returned from a check name availability request. + + :param name_available: Specifies a Boolean value that indicates if the + name is available. + :type name_available: bool + :param name: The name that was checked. + :type name: str + :param message: Message indicating an unavailable name due to a conflict, + or a description of the naming rules that are violated. + :type message: str + :param reason: Message providing the reason why the given name is invalid. + Possible values include: 'Invalid', 'AlreadyExists' + :type reason: str or ~digitaltwins-arm.models.Reason + """ + + _attribute_map = { + 'name_available': {'key': 'nameAvailable', 'type': 'bool'}, + 'name': {'key': 'name', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'reason': {'key': 'reason', 'type': 'str'}, + } + + def __init__(self, *, name_available: bool=None, name: str=None, message: str=None, reason=None, **kwargs) -> None: + super(CheckNameResult, self).__init__(**kwargs) + self.name_available = name_available + self.name = name + self.message = message + self.reason = reason diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description.py new file mode 100644 index 000000000..85c4fe32d --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description.py @@ -0,0 +1,74 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twins_resource import DigitalTwinsResource + + +class DigitalTwinsDescription(DigitalTwinsResource): + """The description of the DigitalTwins service. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: The resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :param location: Required. The resource location. + :type location: str + :param tags: The resource tags. + :type tags: dict[str, str] + :ivar created_time: Time when DigitalTwinsInstance was created. + :vartype created_time: datetime + :ivar last_updated_time: Time when DigitalTwinsInstance was created. + :vartype last_updated_time: datetime + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.ProvisioningState + :ivar host_name: Api endpoint to work with DigitalTwinsInstance. + :vartype host_name: str + """ + + # @digimaun - removed SKU references + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'location': {'required': True}, + 'created_time': {'readonly': True}, + 'last_updated_time': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'host_name': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'location': {'key': 'location', 'type': 'str'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, + 'last_updated_time': {'key': 'properties.lastUpdatedTime', 'type': 'iso-8601'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'host_name': {'key': 'properties.hostName', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsDescription, self).__init__(**kwargs) + self.created_time = None + self.last_updated_time = None + self.provisioning_state = None + self.host_name = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_paged.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_paged.py new file mode 100644 index 000000000..ce5bf3ce1 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_paged.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.paging import Paged + + +class DigitalTwinsDescriptionPaged(Paged): + """ + A paging container for iterating over a list of :class:`DigitalTwinsDescription ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[DigitalTwinsDescription]'} + } + + def __init__(self, *args, **kwargs): + + super(DigitalTwinsDescriptionPaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_py3.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_py3.py new file mode 100644 index 000000000..14309288e --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_py3.py @@ -0,0 +1,74 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twins_resource_py3 import DigitalTwinsResource + + +class DigitalTwinsDescription(DigitalTwinsResource): + """The description of the DigitalTwins service. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: The resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :param location: Required. The resource location. + :type location: str + :param tags: The resource tags. + :type tags: dict[str, str] + :ivar created_time: Time when DigitalTwinsInstance was created. + :vartype created_time: datetime + :ivar last_updated_time: Time when DigitalTwinsInstance was created. + :vartype last_updated_time: datetime + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.ProvisioningState + :ivar host_name: Api endpoint to work with DigitalTwinsInstance. + :vartype host_name: str + """ + + # @digimaun - removed SKU references + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'location': {'required': True}, + 'created_time': {'readonly': True}, + 'last_updated_time': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'host_name': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'location': {'key': 'location', 'type': 'str'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, + 'last_updated_time': {'key': 'properties.lastUpdatedTime', 'type': 'iso-8601'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'host_name': {'key': 'properties.hostName', 'type': 'str'}, + } + + def __init__(self, *, location: str, tags=None, **kwargs) -> None: + super(DigitalTwinsDescription, self).__init__(location=location, tags=tags, **kwargs) + self.created_time = None + self.last_updated_time = None + self.provisioning_state = None + self.host_name = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource.py new file mode 100644 index 000000000..d2abcd93b --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource.py @@ -0,0 +1,59 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .external_resource import ExternalResource + + +class DigitalTwinsEndpointResource(ExternalResource): + """DigitalTwinsInstance endpoint resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: Extension resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'properties.tags', 'type': '{str}'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsEndpointResource, self).__init__(**kwargs) + self.provisioning_state = None + self.created_time = None + self.tags = kwargs.get('tags', None) diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_paged.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_paged.py new file mode 100644 index 000000000..528e8f16d --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_paged.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.paging import Paged + + +class DigitalTwinsEndpointResourcePaged(Paged): + """ + A paging container for iterating over a list of :class:`DigitalTwinsEndpointResource ` object + """ + + # @digimaun - current_page: [DigitalTwinsEndpointResource] -> {object] + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[object]'} + } + + def __init__(self, *args, **kwargs): + + super(DigitalTwinsEndpointResourcePaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties.py new file mode 100644 index 000000000..7fc9a4f71 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties.py @@ -0,0 +1,61 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsEndpointResourceProperties(Model): + """Properties related to Digital Twins Endpoint. + + You probably want to use the sub-classes and not this class directly. Known + sub-classes are: ServiceBus, EventHub, EventGrid + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + :param endpoint_type: Required. Constant filled by server. + :type endpoint_type: str + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + 'endpoint_type': {'required': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, + } + + _subtype_map = { + 'endpoint_type': {'ServiceBus': 'ServiceBus', 'EventHub': 'EventHub', 'EventGrid': 'EventGrid'} + } + + def __init__(self, **kwargs): + super(DigitalTwinsEndpointResourceProperties, self).__init__(**kwargs) + self.provisioning_state = None + self.created_time = None + self.tags = kwargs.get('tags', None) + self.endpoint_type = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties_py3.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties_py3.py new file mode 100644 index 000000000..532622053 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties_py3.py @@ -0,0 +1,61 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsEndpointResourceProperties(Model): + """Properties related to Digital Twins Endpoint. + + You probably want to use the sub-classes and not this class directly. Known + sub-classes are: ServiceBus, EventHub, EventGrid + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + :param endpoint_type: Required. Constant filled by server. + :type endpoint_type: str + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + 'endpoint_type': {'required': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, + } + + _subtype_map = { + 'endpoint_type': {'ServiceBus': 'ServiceBus', 'EventHub': 'EventHub', 'EventGrid': 'EventGrid'} + } + + def __init__(self, *, tags=None, **kwargs) -> None: + super(DigitalTwinsEndpointResourceProperties, self).__init__(**kwargs) + self.provisioning_state = None + self.created_time = None + self.tags = tags + self.endpoint_type = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_py3.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_py3.py new file mode 100644 index 000000000..fdb82c623 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_py3.py @@ -0,0 +1,59 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .external_resource_py3 import ExternalResource + + +class DigitalTwinsEndpointResource(ExternalResource): + """DigitalTwinsInstance endpoint resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: Extension resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'properties.tags', 'type': '{str}'}, + } + + def __init__(self, *, tags=None, **kwargs) -> None: + super(DigitalTwinsEndpointResource, self).__init__(**kwargs) + self.provisioning_state = None + self.created_time = None + self.tags = tags diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description.py new file mode 100644 index 000000000..a9299bfa4 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsPatchDescription(Model): + """The description of the DigitalTwins service. + + :param tags: Instance tags + :type tags: dict[str, str] + """ + + _attribute_map = { + 'tags': {'key': 'tags', 'type': '{str}'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsPatchDescription, self).__init__(**kwargs) + self.tags = kwargs.get('tags', None) diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description_py3.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description_py3.py new file mode 100644 index 000000000..a8e52a902 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description_py3.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsPatchDescription(Model): + """The description of the DigitalTwins service. + + :param tags: Instance tags + :type tags: dict[str, str] + """ + + _attribute_map = { + 'tags': {'key': 'tags', 'type': '{str}'}, + } + + def __init__(self, *, tags=None, **kwargs) -> None: + super(DigitalTwinsPatchDescription, self).__init__(**kwargs) + self.tags = tags diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource.py new file mode 100644 index 000000000..3242e55c4 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource.py @@ -0,0 +1,57 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsResource(Model): + """The common properties of a DigitalTwinsInstance. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: The resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :param location: Required. The resource location. + :type location: str + :param tags: The resource tags. + :type tags: dict[str, str] + """ + + # @digimaun - removed SKU references + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'location': {'required': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'location': {'key': 'location', 'type': 'str'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsResource, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None + self.location = kwargs.get('location', None) + self.tags = kwargs.get('tags', None) diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource_py3.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource_py3.py new file mode 100644 index 000000000..6ef89967f --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource_py3.py @@ -0,0 +1,56 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsResource(Model): + """The common properties of a DigitalTwinsInstance. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: The resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :param location: Required. The resource location. + :type location: str + :param tags: The resource tags. + :type tags: dict[str, str] + """ + # @digimaun - removed SKU references + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'location': {'required': True} + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'location': {'key': 'location', 'type': 'str'}, + 'tags': {'key': 'tags', 'type': '{str}'} + } + + def __init__(self, *, location: str, tags=None, **kwargs) -> None: + super(DigitalTwinsResource, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None + self.location = location + self.tags = tags diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info.py new file mode 100644 index 000000000..ac355d53c --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsSkuInfo(Model): + """Information about the SKU of the DigitalTwinsInstance. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar name: Required. The name of the SKU. Default value: "F1" . + :vartype name: str + """ + + _validation = { + 'name': {'required': True, 'constant': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + } + + name = "F1" diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info_py3.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info_py3.py new file mode 100644 index 000000000..ac355d53c --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info_py3.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsSkuInfo(Model): + """Information about the SKU of the DigitalTwinsInstance. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar name: Required. The name of the SKU. Default value: "F1" . + :vartype name: str + """ + + _validation = { + 'name': {'required': True, 'constant': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + } + + name = "F1" diff --git a/azext_iot/sdk/digitaltwins_arm/models/error_definition.py b/azext_iot/sdk/digitaltwins_arm/models/error_definition.py new file mode 100644 index 000000000..acfd7dc85 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/error_definition.py @@ -0,0 +1,46 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ErrorDefinition(Model): + """Error definition. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar code: Service specific error code which serves as the substatus for + the HTTP error code. + :vartype code: str + :ivar message: Description of the error. + :vartype message: str + :ivar details: Internal error details. + :vartype details: list[~digitaltwins-arm.models.ErrorDefinition] + """ + + _validation = { + 'code': {'readonly': True}, + 'message': {'readonly': True}, + 'details': {'readonly': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'details': {'key': 'details', 'type': '[ErrorDefinition]'}, + } + + def __init__(self, **kwargs): + super(ErrorDefinition, self).__init__(**kwargs) + self.code = None + self.message = None + self.details = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/error_definition_py3.py b/azext_iot/sdk/digitaltwins_arm/models/error_definition_py3.py new file mode 100644 index 000000000..ca558eab6 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/error_definition_py3.py @@ -0,0 +1,46 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ErrorDefinition(Model): + """Error definition. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar code: Service specific error code which serves as the substatus for + the HTTP error code. + :vartype code: str + :ivar message: Description of the error. + :vartype message: str + :ivar details: Internal error details. + :vartype details: list[~digitaltwins-arm.models.ErrorDefinition] + """ + + _validation = { + 'code': {'readonly': True}, + 'message': {'readonly': True}, + 'details': {'readonly': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'details': {'key': 'details', 'type': '[ErrorDefinition]'}, + } + + def __init__(self, **kwargs) -> None: + super(ErrorDefinition, self).__init__(**kwargs) + self.code = None + self.message = None + self.details = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/error_response.py b/azext_iot/sdk/digitaltwins_arm/models/error_response.py new file mode 100644 index 000000000..ffa933ab7 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/error_response.py @@ -0,0 +1,41 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class ErrorResponse(Model): + """Error response. + + :param error: Error description + :type error: ~digitaltwins-arm.models.ErrorDefinition + """ + + _attribute_map = { + 'error': {'key': 'error', 'type': 'ErrorDefinition'}, + } + + def __init__(self, **kwargs): + super(ErrorResponse, self).__init__(**kwargs) + self.error = kwargs.get('error', None) + + +class ErrorResponseException(HttpOperationError): + """Server responsed with exception of type: 'ErrorResponse'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) diff --git a/azext_iot/sdk/digitaltwins_arm/models/error_response_py3.py b/azext_iot/sdk/digitaltwins_arm/models/error_response_py3.py new file mode 100644 index 000000000..9a6d91f8c --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/error_response_py3.py @@ -0,0 +1,41 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class ErrorResponse(Model): + """Error response. + + :param error: Error description + :type error: ~digitaltwins-arm.models.ErrorDefinition + """ + + _attribute_map = { + 'error': {'key': 'error', 'type': 'ErrorDefinition'}, + } + + def __init__(self, *, error=None, **kwargs) -> None: + super(ErrorResponse, self).__init__(**kwargs) + self.error = error + + +class ErrorResponseException(HttpOperationError): + """Server responsed with exception of type: 'ErrorResponse'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) diff --git a/azext_iot/sdk/digitaltwins_arm/models/event_grid.py b/azext_iot/sdk/digitaltwins_arm/models/event_grid.py new file mode 100644 index 000000000..6fb8b38b8 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/event_grid.py @@ -0,0 +1,67 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twins_endpoint_resource_properties import DigitalTwinsEndpointResourceProperties + + +class EventGrid(DigitalTwinsEndpointResourceProperties): + """properties related to eventgrid. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + :param endpoint_type: Required. Constant filled by server. + :type endpoint_type: str + :param topic_endpoint: EventGrid Topic Endpoint + :type topic_endpoint: str + :param access_key1: Required. EventGrid secondary accesskey. Will be + obfuscated during read + :type access_key1: str + :param access_key2: Required. EventGrid secondary accesskey. Will be + obfuscated during read + :type access_key2: str + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + 'endpoint_type': {'required': True}, + 'access_key1': {'required': True}, + 'access_key2': {'required': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, + 'topic_endpoint': {'key': 'TopicEndpoint', 'type': 'str'}, + 'access_key1': {'key': 'accessKey1', 'type': 'str'}, + 'access_key2': {'key': 'accessKey2', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(EventGrid, self).__init__(**kwargs) + self.topic_endpoint = kwargs.get('topic_endpoint', None) + self.access_key1 = kwargs.get('access_key1', None) + self.access_key2 = kwargs.get('access_key2', None) + self.endpoint_type = 'EventGrid' diff --git a/azext_iot/sdk/digitaltwins_arm/models/event_grid_py3.py b/azext_iot/sdk/digitaltwins_arm/models/event_grid_py3.py new file mode 100644 index 000000000..3b445c11f --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/event_grid_py3.py @@ -0,0 +1,67 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twins_endpoint_resource_properties_py3 import DigitalTwinsEndpointResourceProperties + + +class EventGrid(DigitalTwinsEndpointResourceProperties): + """properties related to eventgrid. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + :param endpoint_type: Required. Constant filled by server. + :type endpoint_type: str + :param topic_endpoint: EventGrid Topic Endpoint + :type topic_endpoint: str + :param access_key1: Required. EventGrid secondary accesskey. Will be + obfuscated during read + :type access_key1: str + :param access_key2: Required. EventGrid secondary accesskey. Will be + obfuscated during read + :type access_key2: str + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + 'endpoint_type': {'required': True}, + 'access_key1': {'required': True}, + 'access_key2': {'required': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, + 'topic_endpoint': {'key': 'TopicEndpoint', 'type': 'str'}, + 'access_key1': {'key': 'accessKey1', 'type': 'str'}, + 'access_key2': {'key': 'accessKey2', 'type': 'str'}, + } + + def __init__(self, *, access_key1: str, access_key2: str, tags=None, topic_endpoint: str=None, **kwargs) -> None: + super(EventGrid, self).__init__(tags=tags, **kwargs) + self.topic_endpoint = topic_endpoint + self.access_key1 = access_key1 + self.access_key2 = access_key2 + self.endpoint_type = 'EventGrid' diff --git a/azext_iot/sdk/digitaltwins_arm/models/event_hub.py b/azext_iot/sdk/digitaltwins_arm/models/event_hub.py new file mode 100644 index 000000000..a4a06edf6 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/event_hub.py @@ -0,0 +1,63 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twins_endpoint_resource_properties import DigitalTwinsEndpointResourceProperties + + +class EventHub(DigitalTwinsEndpointResourceProperties): + """properties related to eventhub. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + :param endpoint_type: Required. Constant filled by server. + :type endpoint_type: str + :param connection_string_primary_key: Required. PrimaryConnectionString of + the endpoint. Will be obfuscated during read + :type connection_string_primary_key: str + :param connection_string_secondary_key: Required. + SecondaryConnectionString of the endpoint. Will be obfuscated during read + :type connection_string_secondary_key: str + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + 'endpoint_type': {'required': True}, + 'connection_string_primary_key': {'required': True}, + 'connection_string_secondary_key': {'required': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, + 'connection_string_primary_key': {'key': 'connectionString-PrimaryKey', 'type': 'str'}, + 'connection_string_secondary_key': {'key': 'connectionString-SecondaryKey', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(EventHub, self).__init__(**kwargs) + self.connection_string_primary_key = kwargs.get('connection_string_primary_key', None) + self.connection_string_secondary_key = kwargs.get('connection_string_secondary_key', None) + self.endpoint_type = 'EventHub' diff --git a/azext_iot/sdk/digitaltwins_arm/models/event_hub_py3.py b/azext_iot/sdk/digitaltwins_arm/models/event_hub_py3.py new file mode 100644 index 000000000..204923f9a --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/event_hub_py3.py @@ -0,0 +1,63 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twins_endpoint_resource_properties_py3 import DigitalTwinsEndpointResourceProperties + + +class EventHub(DigitalTwinsEndpointResourceProperties): + """properties related to eventhub. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + :param endpoint_type: Required. Constant filled by server. + :type endpoint_type: str + :param connection_string_primary_key: Required. PrimaryConnectionString of + the endpoint. Will be obfuscated during read + :type connection_string_primary_key: str + :param connection_string_secondary_key: Required. + SecondaryConnectionString of the endpoint. Will be obfuscated during read + :type connection_string_secondary_key: str + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + 'endpoint_type': {'required': True}, + 'connection_string_primary_key': {'required': True}, + 'connection_string_secondary_key': {'required': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, + 'connection_string_primary_key': {'key': 'connectionString-PrimaryKey', 'type': 'str'}, + 'connection_string_secondary_key': {'key': 'connectionString-SecondaryKey', 'type': 'str'}, + } + + def __init__(self, *, connection_string_primary_key: str, connection_string_secondary_key: str, tags=None, **kwargs) -> None: + super(EventHub, self).__init__(tags=tags, **kwargs) + self.connection_string_primary_key = connection_string_primary_key + self.connection_string_secondary_key = connection_string_secondary_key + self.endpoint_type = 'EventHub' diff --git a/azext_iot/sdk/digitaltwins_arm/models/external_resource.py b/azext_iot/sdk/digitaltwins_arm/models/external_resource.py new file mode 100644 index 000000000..695691d7a --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/external_resource.py @@ -0,0 +1,45 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ExternalResource(Model): + """Definition of a Resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: Extension resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ExternalResource, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/external_resource_py3.py b/azext_iot/sdk/digitaltwins_arm/models/external_resource_py3.py new file mode 100644 index 000000000..aefe1ccdb --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/external_resource_py3.py @@ -0,0 +1,45 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ExternalResource(Model): + """Definition of a Resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: Extension resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs) -> None: + super(ExternalResource, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource.py new file mode 100644 index 000000000..568b35915 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/integration_resource.py @@ -0,0 +1,61 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .external_resource import ExternalResource + + +class IntegrationResource(ExternalResource): + """IoTHub integration resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: Extension resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. + Possible values include: 'Provisioning', 'Deleting', 'Succeeded', + 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.IntegrationResourceState + :param resource_id: Fully qualified resource identifier of the + DigitalTwins Azure resource. + :type resource_id: str + :ivar created_time: Time when the IoTHub was added to + DigitalTwinsInstance. + :vartype created_time: datetime + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'resource_id': {'key': 'properties.resourceId', 'type': 'str'}, + 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, + } + + def __init__(self, **kwargs): + super(IntegrationResource, self).__init__(**kwargs) + self.provisioning_state = None + self.resource_id = kwargs.get('resource_id', None) + self.created_time = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_paged.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_paged.py new file mode 100644 index 000000000..4fc4e48b4 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_paged.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.paging import Paged + + +class IntegrationResourcePaged(Paged): + """ + A paging container for iterating over a list of :class:`IntegrationResource ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[IntegrationResource]'} + } + + def __init__(self, *args, **kwargs): + + super(IntegrationResourcePaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_py3.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_py3.py new file mode 100644 index 000000000..29d42c445 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_py3.py @@ -0,0 +1,61 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .external_resource_py3 import ExternalResource + + +class IntegrationResource(ExternalResource): + """IoTHub integration resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: Extension resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. + Possible values include: 'Provisioning', 'Deleting', 'Succeeded', + 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.IntegrationResourceState + :param resource_id: Fully qualified resource identifier of the + DigitalTwins Azure resource. + :type resource_id: str + :ivar created_time: Time when the IoTHub was added to + DigitalTwinsInstance. + :vartype created_time: datetime + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'resource_id': {'key': 'properties.resourceId', 'type': 'str'}, + 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, + } + + def __init__(self, *, resource_id: str=None, **kwargs) -> None: + super(IntegrationResource, self).__init__(**kwargs) + self.provisioning_state = None + self.resource_id = resource_id + self.created_time = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1.py new file mode 100644 index 000000000..d822089c9 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1.py @@ -0,0 +1,38 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IntegrationResourceState1(Model): + """Properties related to the IoTHub DigitalTwinsInstance Integration Resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. + Possible values include: 'Provisioning', 'Deleting', 'Succeeded', + 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.IntegrationResourceState + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(IntegrationResourceState1, self).__init__(**kwargs) + self.provisioning_state = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1_py3.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1_py3.py new file mode 100644 index 000000000..c8dad07fb --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1_py3.py @@ -0,0 +1,38 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IntegrationResourceState1(Model): + """Properties related to the IoTHub DigitalTwinsInstance Integration Resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. + Possible values include: 'Provisioning', 'Deleting', 'Succeeded', + 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.IntegrationResourceState + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + } + + def __init__(self, **kwargs) -> None: + super(IntegrationResourceState1, self).__init__(**kwargs) + self.provisioning_state = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info.py new file mode 100644 index 000000000..8b8393ea6 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info.py @@ -0,0 +1,50 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .external_resource import ExternalResource + + +class IntegrationResourceUpdateInfo(ExternalResource): + """IoTHub integration resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: Extension resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. + Possible values include: 'Provisioning', 'Deleting', 'Succeeded', + 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.IntegrationResourceState + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(IntegrationResourceUpdateInfo, self).__init__(**kwargs) + self.provisioning_state = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info_py3.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info_py3.py new file mode 100644 index 000000000..18605393a --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info_py3.py @@ -0,0 +1,50 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .external_resource_py3 import ExternalResource + + +class IntegrationResourceUpdateInfo(ExternalResource): + """IoTHub integration resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: Extension resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. + Possible values include: 'Provisioning', 'Deleting', 'Succeeded', + 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.IntegrationResourceState + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + } + + def __init__(self, **kwargs) -> None: + super(IntegrationResourceUpdateInfo, self).__init__(**kwargs) + self.provisioning_state = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation.py b/azext_iot/sdk/digitaltwins_arm/models/operation.py new file mode 100644 index 000000000..a26283e41 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/operation.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Operation(Model): + """DigitalTwins service REST API operation. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar name: Operation name: {provider}/{resource}/{read | write | action | + delete} + :vartype name: str + :param display: Operation properties display + :type display: ~digitaltwins-arm.models.OperationDisplay + """ + + _validation = { + 'name': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'display': {'key': 'display', 'type': 'OperationDisplay'}, + } + + def __init__(self, **kwargs): + super(Operation, self).__init__(**kwargs) + self.name = None + self.display = kwargs.get('display', None) diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation_display.py b/azext_iot/sdk/digitaltwins_arm/models/operation_display.py new file mode 100644 index 000000000..8db099ffc --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/operation_display.py @@ -0,0 +1,50 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class OperationDisplay(Model): + """The object that represents the operation. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar provider: Service provider: Microsoft DigitalTwins + :vartype provider: str + :ivar resource: Resource Type: DigitalTwinsInstances + :vartype resource: str + :ivar operation: Name of the operation + :vartype operation: str + :ivar description: Friendly description for the operation, + :vartype description: str + """ + + _validation = { + 'provider': {'readonly': True}, + 'resource': {'readonly': True}, + 'operation': {'readonly': True}, + 'description': {'readonly': True}, + } + + _attribute_map = { + 'provider': {'key': 'provider', 'type': 'str'}, + 'resource': {'key': 'resource', 'type': 'str'}, + 'operation': {'key': 'operation', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(OperationDisplay, self).__init__(**kwargs) + self.provider = None + self.resource = None + self.operation = None + self.description = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation_display_py3.py b/azext_iot/sdk/digitaltwins_arm/models/operation_display_py3.py new file mode 100644 index 000000000..82c6ceb80 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/operation_display_py3.py @@ -0,0 +1,50 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class OperationDisplay(Model): + """The object that represents the operation. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar provider: Service provider: Microsoft DigitalTwins + :vartype provider: str + :ivar resource: Resource Type: DigitalTwinsInstances + :vartype resource: str + :ivar operation: Name of the operation + :vartype operation: str + :ivar description: Friendly description for the operation, + :vartype description: str + """ + + _validation = { + 'provider': {'readonly': True}, + 'resource': {'readonly': True}, + 'operation': {'readonly': True}, + 'description': {'readonly': True}, + } + + _attribute_map = { + 'provider': {'key': 'provider', 'type': 'str'}, + 'resource': {'key': 'resource', 'type': 'str'}, + 'operation': {'key': 'operation', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + } + + def __init__(self, **kwargs) -> None: + super(OperationDisplay, self).__init__(**kwargs) + self.provider = None + self.resource = None + self.operation = None + self.description = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation_paged.py b/azext_iot/sdk/digitaltwins_arm/models/operation_paged.py new file mode 100644 index 000000000..6ce7d4c1c --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/operation_paged.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.paging import Paged + + +class OperationPaged(Paged): + """ + A paging container for iterating over a list of :class:`Operation ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[Operation]'} + } + + def __init__(self, *args, **kwargs): + + super(OperationPaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation_py3.py b/azext_iot/sdk/digitaltwins_arm/models/operation_py3.py new file mode 100644 index 000000000..24014c3b3 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/operation_py3.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Operation(Model): + """DigitalTwins service REST API operation. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar name: Operation name: {provider}/{resource}/{read | write | action | + delete} + :vartype name: str + :param display: Operation properties display + :type display: ~digitaltwins-arm.models.OperationDisplay + """ + + _validation = { + 'name': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'display': {'key': 'display', 'type': 'OperationDisplay'}, + } + + def __init__(self, *, display=None, **kwargs) -> None: + super(Operation, self).__init__(**kwargs) + self.name = None + self.display = display diff --git a/azext_iot/sdk/digitaltwins_arm/models/service_bus.py b/azext_iot/sdk/digitaltwins_arm/models/service_bus.py new file mode 100644 index 000000000..133c04e22 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/service_bus.py @@ -0,0 +1,63 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twins_endpoint_resource_properties import DigitalTwinsEndpointResourceProperties + + +class ServiceBus(DigitalTwinsEndpointResourceProperties): + """properties related to servicebus. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + :param endpoint_type: Required. Constant filled by server. + :type endpoint_type: str + :param primary_connection_string: Required. PrimaryConnectionString of the + endpoint. Will be obfuscated during read + :type primary_connection_string: str + :param secondary_connection_string: Required. SecondaryConnectionString of + the endpoint. Will be obfuscated during read + :type secondary_connection_string: str + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + 'endpoint_type': {'required': True}, + 'primary_connection_string': {'required': True}, + 'secondary_connection_string': {'required': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, + 'primary_connection_string': {'key': 'primaryConnectionString', 'type': 'str'}, + 'secondary_connection_string': {'key': 'secondaryConnectionString', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ServiceBus, self).__init__(**kwargs) + self.primary_connection_string = kwargs.get('primary_connection_string', None) + self.secondary_connection_string = kwargs.get('secondary_connection_string', None) + self.endpoint_type = 'ServiceBus' diff --git a/azext_iot/sdk/digitaltwins_arm/models/service_bus_py3.py b/azext_iot/sdk/digitaltwins_arm/models/service_bus_py3.py new file mode 100644 index 000000000..84a2a7ab6 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/models/service_bus_py3.py @@ -0,0 +1,63 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twins_endpoint_resource_properties_py3 import DigitalTwinsEndpointResourceProperties + + +class ServiceBus(DigitalTwinsEndpointResourceProperties): + """properties related to servicebus. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~digitaltwins-arm.models.EndpointProvisioningState + :ivar created_time: Time when the Endpoint was added to + DigitalTwinsInstance. + :vartype created_time: datetime + :param tags: The resource tags. + :type tags: dict[str, str] + :param endpoint_type: Required. Constant filled by server. + :type endpoint_type: str + :param primary_connection_string: Required. PrimaryConnectionString of the + endpoint. Will be obfuscated during read + :type primary_connection_string: str + :param secondary_connection_string: Required. SecondaryConnectionString of + the endpoint. Will be obfuscated during read + :type secondary_connection_string: str + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + 'created_time': {'readonly': True}, + 'endpoint_type': {'required': True}, + 'primary_connection_string': {'required': True}, + 'secondary_connection_string': {'required': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, + 'primary_connection_string': {'key': 'primaryConnectionString', 'type': 'str'}, + 'secondary_connection_string': {'key': 'secondaryConnectionString', 'type': 'str'}, + } + + def __init__(self, *, primary_connection_string: str, secondary_connection_string: str, tags=None, **kwargs) -> None: + super(ServiceBus, self).__init__(tags=tags, **kwargs) + self.primary_connection_string = primary_connection_string + self.secondary_connection_string = secondary_connection_string + self.endpoint_type = 'ServiceBus' diff --git a/azext_iot/sdk/digitaltwins_arm/operations/__init__.py b/azext_iot/sdk/digitaltwins_arm/operations/__init__.py new file mode 100644 index 000000000..d55a7f121 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/operations/__init__.py @@ -0,0 +1,20 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twins_operations import DigitalTwinsOperations +from .digital_twins_endpoint_operations import DigitalTwinsEndpointOperations +from .operations import Operations + +__all__ = [ + 'DigitalTwinsOperations', + 'DigitalTwinsEndpointOperations', + 'Operations', +] diff --git a/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_endpoint_operations.py b/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_endpoint_operations.py new file mode 100644 index 000000000..e569ac027 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_endpoint_operations.py @@ -0,0 +1,382 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrest.polling import LROPoller, NoPolling +from msrestazure.polling.arm_polling import ARMPolling + +from .. import models + + +class DigitalTwinsEndpointOperations(object): + """DigitalTwinsEndpointOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-03-01-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-03-01-preview" + + self.config = config + + def list( + self, resource_group_name, resource_name, custom_headers=None, raw=False, **operation_config): + """Get DigitalTwinsInstance Endpoints. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of DigitalTwinsEndpointResource + :rtype: + ~digitaltwins-arm.models.DigitalTwinsEndpointResourcePaged[~digitaltwins-arm.models.DigitalTwinsEndpointResource] + :raises: + :class:`ErrorResponseException` + """ + def internal_paging(next_link=None, raw=False): + + if not next_link: + # Construct URL + url = self.list.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + deserialized = models.DigitalTwinsEndpointResourcePaged(internal_paging, self._deserialize.dependencies) + + if raw: + header_dict = {} + client_raw_response = models.DigitalTwinsEndpointResourcePaged(internal_paging, self._deserialize.dependencies, header_dict) + return client_raw_response + + return deserialized + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}/endpoints'} + + def get( + self, resource_group_name, resource_name, endpoint_name, custom_headers=None, raw=False, **operation_config): + """Get DigitalTwinsInstances Endpoint. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param endpoint_name: Name of Endpoint Resource. + :type endpoint_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: DigitalTwinsEndpointResource or ClientRawResponse if raw=true + :rtype: ~digitaltwins-arm.models.DigitalTwinsEndpointResource or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1), + 'endpointName': self._serialize.url("endpoint_name", endpoint_name, 'str', max_length=64, min_length=1, pattern=r'^[A-Za-z0-9-._]{1,64}$') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + # @digimaun - DigitalTwinsEndpointResource -> {object} + if response.status_code == 200: + deserialized = self._deserialize('{object}', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}/endpoints/{endpointName}'} + + # @digimaun - custom add properties + def _create_or_update_initial( + self, resource_group_name, resource_name, endpoint_name, properties, custom_headers=None, raw=False, **operation_config): + endpoint_description = properties + + # Construct URL + url = self.create_or_update.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1), + 'endpointName': self._serialize.url("endpoint_name", endpoint_name, 'str', max_length=64, min_length=1, pattern=r'^[A-Za-z0-9-._]{1,64}$') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(endpoint_description, "{object}") + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 201]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + if response.status_code in [200, 201]: + deserialized = self._deserialize("{object}", response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + # @digimaun - custom add properties + def create_or_update( + self, resource_group_name, resource_name, endpoint_name, properties, custom_headers=None, raw=False, polling=True, **operation_config): + """Create or update DigitalTwinsInstance endpoint. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param endpoint_name: Name of Endpoint Resource. + :type endpoint_name: str + :param tags: The resource tags. + :type tags: dict[str, str] + :param dict custom_headers: headers that will be added to the request + :param bool raw: The poller return type is ClientRawResponse, the + direct response alongside the deserialized response + :param polling: True for ARMPolling, False for no polling, or a + polling object for personal polling strategy + :return: An instance of LROPoller that returns + DigitalTwinsEndpointResource or + ClientRawResponse if raw==True + :rtype: + ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsEndpointResource] + or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsEndpointResource]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._create_or_update_initial( + resource_group_name=resource_group_name, + resource_name=resource_name, + endpoint_name=endpoint_name, + properties=properties, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + # @digimaun - DigitalTwinsEndpointResource -> {object} + def get_long_running_output(response): + deserialized = self._deserialize('{object}', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}/endpoints/{endpointName}'} + + + def _delete_initial( + self, resource_group_name, resource_name, endpoint_name, custom_headers=None, raw=False, **operation_config): + # Construct URL + url = self.delete.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1), + 'endpointName': self._serialize.url("endpoint_name", endpoint_name, 'str', max_length=64, min_length=1, pattern=r'^[A-Za-z0-9-._]{1,64}$') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 202, 204]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + if response.status_code == 202: + deserialized = self._deserialize('DigitalTwinsEndpointResource', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + def delete( + self, resource_group_name, resource_name, endpoint_name, custom_headers=None, raw=False, polling=True, **operation_config): + """Delete a DigitalTwinsInstance endpoint. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param endpoint_name: Name of Endpoint Resource. + :type endpoint_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: The poller return type is ClientRawResponse, the + direct response alongside the deserialized response + :param polling: True for ARMPolling, False for no polling, or a + polling object for personal polling strategy + :return: An instance of LROPoller that returns + DigitalTwinsEndpointResource or + ClientRawResponse if raw==True + :rtype: + ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsEndpointResource] + or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsEndpointResource]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._delete_initial( + resource_group_name=resource_group_name, + resource_name=resource_name, + endpoint_name=endpoint_name, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + def get_long_running_output(response): + deserialized = self._deserialize('DigitalTwinsEndpointResource', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}/endpoints/{endpointName}'} diff --git a/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py b/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py new file mode 100644 index 000000000..d7685dbe7 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py @@ -0,0 +1,602 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrest.polling import LROPoller, NoPolling +from msrestazure.polling.arm_polling import ARMPolling + +from .. import models + + +class DigitalTwinsOperations(object): + """DigitalTwinsOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-03-01-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-03-01-preview" + + self.config = config + + def get( + self, resource_group_name, resource_name, custom_headers=None, raw=False, **operation_config): + """Get DigitalTwinsInstances resource. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: DigitalTwinsDescription or ClientRawResponse if raw=true + :rtype: ~digitaltwins-arm.models.DigitalTwinsDescription or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('DigitalTwinsDescription', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}'} + + def _create_or_update_initial( + self, resource_group_name, resource_name, location, tags=None, custom_headers=None, raw=False, **operation_config): + digital_twins_create = models.DigitalTwinsDescription(location=location, tags=tags) + + # Construct URL + url = self.create_or_update.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(digital_twins_create, 'DigitalTwinsDescription') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 201]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('DigitalTwinsDescription', response) + if response.status_code == 201: + deserialized = self._deserialize('DigitalTwinsDescription', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + def create_or_update( + self, resource_group_name, resource_name, location, tags=None, custom_headers=None, raw=False, polling=True, **operation_config): + """Create or update the metadata of a DigitalTwinsInstance. The usual + pattern to modify a property is to retrieve the DigitalTwinsInstance + and security metadata, and then combine them with the modified values + in a new body to update the DigitalTwinsInstance. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param location: The resource location. + :type location: str + :param tags: The resource tags. + :type tags: dict[str, str] + :param dict custom_headers: headers that will be added to the request + :param bool raw: The poller return type is ClientRawResponse, the + direct response alongside the deserialized response + :param polling: True for ARMPolling, False for no polling, or a + polling object for personal polling strategy + :return: An instance of LROPoller that returns DigitalTwinsDescription + or ClientRawResponse if raw==True + :rtype: + ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsDescription] + or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsDescription]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._create_or_update_initial( + resource_group_name=resource_group_name, + resource_name=resource_name, + location=location, + tags=tags, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + def get_long_running_output(response): + deserialized = self._deserialize('DigitalTwinsDescription', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}'} + + + def _update_initial( + self, resource_group_name, resource_name, tags=None, custom_headers=None, raw=False, **operation_config): + digital_twins_patch_description = models.DigitalTwinsPatchDescription(tags=tags) + + # Construct URL + url = self.update.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(digital_twins_patch_description, 'DigitalTwinsPatchDescription') + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 201]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('DigitalTwinsDescription', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + def update( + self, resource_group_name, resource_name, tags=None, custom_headers=None, raw=False, polling=True, **operation_config): + """Update metadata of DigitalTwinsInstance. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param tags: Instance tags + :type tags: dict[str, str] + :param dict custom_headers: headers that will be added to the request + :param bool raw: The poller return type is ClientRawResponse, the + direct response alongside the deserialized response + :param polling: True for ARMPolling, False for no polling, or a + polling object for personal polling strategy + :return: An instance of LROPoller that returns DigitalTwinsDescription + or ClientRawResponse if raw==True + :rtype: + ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsDescription] + or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsDescription]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._update_initial( + resource_group_name=resource_group_name, + resource_name=resource_name, + tags=tags, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + def get_long_running_output(response): + deserialized = self._deserialize('DigitalTwinsDescription', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}'} + + + def _delete_initial( + self, resource_group_name, resource_name, custom_headers=None, raw=False, **operation_config): + # Construct URL + url = self.delete.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 202, 204]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + if response.status_code == 202: + # @digimaun - DigitalTwinsDescription -> {object} + deserialized = self._deserialize('{object}', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + def delete( + self, resource_group_name, resource_name, custom_headers=None, raw=False, polling=True, **operation_config): + """Delete a DigitalTwinsInstance. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: The poller return type is ClientRawResponse, the + direct response alongside the deserialized response + :param polling: True for ARMPolling, False for no polling, or a + polling object for personal polling strategy + :return: An instance of LROPoller that returns DigitalTwinsDescription + or ClientRawResponse if raw==True + :rtype: + ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsDescription] + or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsDescription]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._delete_initial( + resource_group_name=resource_group_name, + resource_name=resource_name, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + # @digimaun - DigitalTwinsDescription -> {object} + def get_long_running_output(response): + deserialized = self._deserialize('{object}', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}'} + + def list( + self, custom_headers=None, raw=False, **operation_config): + """Get all the DigitalTwinsInstances in a subscription. + + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of DigitalTwinsDescription + :rtype: + ~digitaltwins-arm.models.DigitalTwinsDescriptionPaged[~digitaltwins-arm.models.DigitalTwinsDescription] + :raises: + :class:`ErrorResponseException` + """ + def internal_paging(next_link=None, raw=False): + if not next_link: + # Construct URL + url = self.list.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + deserialized = models.DigitalTwinsDescriptionPaged(internal_paging, self._deserialize.dependencies) + + if raw: + header_dict = {} + client_raw_response = models.DigitalTwinsDescriptionPaged(internal_paging, self._deserialize.dependencies, header_dict) + return client_raw_response + + return deserialized + list.metadata = {'url': '/subscriptions/{subscriptionId}/providers/Microsoft.DigitalTwins/digitalTwinsInstances'} + + def list_by_resource_group( + self, resource_group_name, custom_headers=None, raw=False, **operation_config): + """Get all the DigitalTwinsInstances in a resource group. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of DigitalTwinsDescription + :rtype: + ~digitaltwins-arm.models.DigitalTwinsDescriptionPaged[~digitaltwins-arm.models.DigitalTwinsDescription] + :raises: + :class:`ErrorResponseException` + """ + def internal_paging(next_link=None, raw=False): + + if not next_link: + # Construct URL + url = self.list_by_resource_group.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1) + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + deserialized = models.DigitalTwinsDescriptionPaged(internal_paging, self._deserialize.dependencies) + + if raw: + header_dict = {} + client_raw_response = models.DigitalTwinsDescriptionPaged(internal_paging, self._deserialize.dependencies, header_dict) + return client_raw_response + + return deserialized + list_by_resource_group.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances'} + + def check_name_availability( + self, location, name, custom_headers=None, raw=False, **operation_config): + """Check if a DigitalTwinsInstance name is available. + + :param location: Location of DigitalTwinsInstance. + :type location: str + :param name: Resource name. + :type name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: CheckNameResult or ClientRawResponse if raw=true + :rtype: ~digitaltwins-arm.models.CheckNameResult or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + digital_twins_instance_check_name = models.CheckNameRequest(name=name) + + # Construct URL + url = self.check_name_availability.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'location': self._serialize.url("location", location, 'str', min_length=3) + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(digital_twins_instance_check_name, 'CheckNameRequest') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('CheckNameResult', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + check_name_availability.metadata = {'url': '/subscriptions/{subscriptionId}/providers/Microsoft.DigitalTwins/locations/{location}/checkNameAvailability'} diff --git a/azext_iot/sdk/digitaltwins_arm/operations/operations.py b/azext_iot/sdk/digitaltwins_arm/operations/operations.py new file mode 100644 index 000000000..dc525f21c --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/operations/operations.py @@ -0,0 +1,96 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class Operations(object): + """Operations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-03-01-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-03-01-preview" + + self.config = config + + def list( + self, custom_headers=None, raw=False, **operation_config): + """Lists all of the available DigitalTwins service REST API operations. + + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of Operation + :rtype: + ~digitaltwins-arm.models.OperationPaged[~digitaltwins-arm.models.Operation] + :raises: + :class:`ErrorResponseException` + """ + def internal_paging(next_link=None, raw=False): + + if not next_link: + # Construct URL + url = self.list.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str', min_length=10) + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + deserialized = models.OperationPaged(internal_paging, self._deserialize.dependencies) + + if raw: + header_dict = {} + client_raw_response = models.OperationPaged(internal_paging, self._deserialize.dependencies, header_dict) + return client_raw_response + + return deserialized + list.metadata = {'url': '/providers/Microsoft.DigitalTwins/operations'} diff --git a/azext_iot/sdk/digitaltwins_arm/version.py b/azext_iot/sdk/digitaltwins_arm/version.py new file mode 100644 index 000000000..be153c187 --- /dev/null +++ b/azext_iot/sdk/digitaltwins_arm/version.py @@ -0,0 +1,12 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +VERSION = "2020-03-01-preview" diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py new file mode 100644 index 000000000..6a7408a1b --- /dev/null +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -0,0 +1,78 @@ +# 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 +import os +from ..generators import generate_generic_id +from azure.cli.testsdk import LiveScenarioTest + +MOCK_RESOURCE_TAGS = "a=b;c=d" +MOCK_ENDPOINT_TAGS = "key0=value0;key1=value1;" + + +def generate_resource_id(): + return "dt{}".format(generate_generic_id()) + + +def generate_group_id(): + return "group{}".format(generate_generic_id()) + + +class DTLiveScenarioTest(LiveScenarioTest): + group_location = "westus2" + group_names = [] + dt_location_default = "eastus2euap" + + role_map = { + "owner": "Azure Digital Twins Owner (Preview)", + "reader": "Azure Digital Twins Reader (Preview)", + } + + def __init__(self, test_scenario, group_names): + assert test_scenario + assert group_names + + os.environ["AZURE_CORE_COLLECT_TELEMETRY"] = "no" + super(DTLiveScenarioTest, self).__init__(test_scenario) + DTLiveScenarioTest.handle = self + + DTLiveScenarioTest.group_names = group_names + self._bootup_scenario() + + def _bootup_scenario(self): + self._is_provider_registered() + for group_name in self.group_names: + self.cmd("group create -n {} -l {}".format(group_name, self.group_location)) + + def _is_provider_registered(self): + result = self.cmd( + "provider show --namespace 'Microsoft.DigitalTwins' --query 'registrationState'" + ) + if '"registered"' in result.output.lower(): + return + pytest.skip( + "Microsoft.DigitalTwins provider not registered. " + "Run 'az provider register --namespace Microsoft.DigitalTwins'" + ) + + @property + def dt_location(self): + return self.dt_location_default + + @dt_location.setter + def dt_location(self, value): + self.dt_location_default = value + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + target_groups = DTLiveScenarioTest.group_names + # Ensure clean-up after ourselves. + for group in target_groups: + try: + cls.handle.cmd("group delete -n {} -y --no-wait".format(group)) + except: + pass diff --git a/azext_iot/tests/digitaltwins/conftest.py b/azext_iot/tests/digitaltwins/conftest.py new file mode 100644 index 000000000..fd4d03453 --- /dev/null +++ b/azext_iot/tests/digitaltwins/conftest.py @@ -0,0 +1,14 @@ +# 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.digitaltwins.providers.resource import ResourceProvider + + +@pytest.fixture +def dt_rp(fixture_cmd2): + rp = ResourceProvider(fixture_cmd2) + yield rp diff --git a/azext_iot/tests/digitaltwins/models/Floor.json b/azext_iot/tests/digitaltwins/models/Floor.json new file mode 100644 index 000000000..58969594d --- /dev/null +++ b/azext_iot/tests/digitaltwins/models/Floor.json @@ -0,0 +1,22 @@ +{ + "@id": "dtmi:example:Floor;1", + "@type": "Interface", + "@context": "dtmi:dtdl:context;2", + "displayName": "Floor", + "contents": [{ + "@type": "Relationship", + "@id": "dtmi:contosocom:DigitalTwins:contains;1", + "name": "contains", + "properties": [{ + "@type": "Property", + "name": "ownershipUser", + "schema": "string" + }, + { + "@type": "Property", + "name": "ownershipDepartment", + "schema": "string" + } + ] + }] +} \ No newline at end of file diff --git a/azext_iot/tests/digitaltwins/models/nested/Room.json b/azext_iot/tests/digitaltwins/models/nested/Room.json new file mode 100644 index 000000000..03ce6cdee --- /dev/null +++ b/azext_iot/tests/digitaltwins/models/nested/Room.json @@ -0,0 +1,22 @@ +{ + "@id": "dtmi:example:Room;1", + "@type": "Interface", + "@context": "dtmi:dtdl:context;2", + "displayName": "Room", + "contents": [{ + "@type": "Property", + "name": "Temperature", + "schema": "double" + }, + { + "@type": "Property", + "name": "Humidity", + "schema": "double" + }, + { + "@type": "Component", + "name": "Thermostat", + "schema": "dtmi:com:example:Thermostat;1" + } + ] +} \ No newline at end of file diff --git a/azext_iot/tests/digitaltwins/models/nested/deeper/Thermostat.json b/azext_iot/tests/digitaltwins/models/nested/deeper/Thermostat.json new file mode 100644 index 000000000..803dc5e84 --- /dev/null +++ b/azext_iot/tests/digitaltwins/models/nested/deeper/Thermostat.json @@ -0,0 +1,18 @@ +{ + "@id": "dtmi:com:example:Thermostat;1", + "@type": "Interface", + "displayName": "Thermostat", + "contents": [{ + "@type": "Telemetry", + "name": "temp", + "schema": "double" + }, + { + "@type": "Property", + "name": "setPointTemp", + "writable": true, + "schema": "double" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py new file mode 100644 index 000000000..77b5acc42 --- /dev/null +++ b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py @@ -0,0 +1,215 @@ +# 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 +import json +from time import sleep +from knack.log import get_logger +from azext_iot.common.utility import ( + scantree, + process_json_arg, +) +from ..settings import DynamoSettings +from . import DTLiveScenarioTest +from . import ( + generate_resource_id, + generate_group_id, +) + +logger = get_logger(__name__) + +model_tests_env_vars = [ + "azext_dt_rbac_assignee_owner", +] + +settings = DynamoSettings([], model_tests_env_vars) +run_tests = False + +if all([settings.env.azext_dt_rbac_assignee_owner]): + run_tests = True + + +@pytest.mark.skipif(not run_tests, reason="azext_dt_rbac_assignee_owner is required.") +@pytest.mark.usefixtures("set_cwd") +class TestDTModelLifecycle(DTLiveScenarioTest): + def __init__(self, test_case): + self.group_names = [generate_group_id()] + self.rbac_assignee_owner = settings.env.azext_dt_rbac_assignee_owner + self.dt_location = "westus2" + self.room_dtmi = "dtmi:example:Room;1" + self.floor_dtmi = "dtmi:example:Floor;1" + + super(TestDTModelLifecycle, self).__init__(test_case, self.group_names) + + def test_dt_models(self): + instance_name = generate_resource_id() + models_directory = "./models" + inline_model = "./models/Floor.json" + component_id = "dtmi:com:example:Thermostat;1" + + self.cmd( + "dt create -n {} -g {} -l {}".format( + instance_name, self.group_names[0], self.dt_location + ) + ) + + self.cmd( + "dt role-assignment create -n {} -g {} --assignee {} --role '{}'".format( + instance_name, self.group_names[0], self.rbac_assignee_owner, self.role_map["owner"] + ) + ) + # Wait for RBAC to catch-up + sleep(10) + + create_models_output = self.cmd( + "dt model create -n {} --from-directory '{}'".format( + instance_name, models_directory + ) + ).get_output_in_json() + + assert_create_models_attributes( + create_models_output, directory_path=models_directory + ) + + list_models_output = self.cmd( + "dt model list -n {}".format(instance_name) + ).get_output_in_json() + assert len(list_models_output) == len(create_models_output) + for model in list_models_output: + assert model["id"] + + list_models_output = self.cmd( + "dt model list -n {} -g {} --definition".format( + instance_name, self.group_names[0] + ) + ).get_output_in_json() + assert len(list_models_output) == len(create_models_output) + for model in list_models_output: + assert model["id"] + assert model["model"] + + model_dependencies_output = self.cmd( + "dt model list -n {} -g {} --dependencies-for '{}'".format( + instance_name, self.group_names[0], self.room_dtmi, + ) + ).get_output_in_json() + assert len(model_dependencies_output) == 2 + + for model in create_models_output: + model_show_output = self.cmd( + "dt model show -n {} --dtmi '{}'".format(instance_name, model["id"]) + ).get_output_in_json() + assert model_show_output["id"] == model["id"] + + model_show_def_output = self.cmd( + "dt model show -n {} -g {} --dtmi '{}' --definition".format( + instance_name, self.group_names[0], model["id"] + ) + ).get_output_in_json() + + assert model_show_def_output["id"] == model["id"] + assert model_show_def_output["model"] + assert model_show_def_output["model"]["@id"] == model["id"] + + model_json = process_json_arg(inline_model, "models") + model_id = model_json["@id"] + inc_model_id = _increment_model_id(model_id) + model_json["@id"] = inc_model_id + self.kwargs["modelJson"] = json.dumps(model_json) + create_models_inline_output = self.cmd( + "dt model create -n {} --models '{}'".format(instance_name, "{modelJson}") + ).get_output_in_json() + assert create_models_inline_output[0]["id"] == inc_model_id + + update_model_output = self.cmd( + "dt model update -n {} --dtmi '{}' --decommission".format( + instance_name, inc_model_id + ) + ).get_output_in_json() + assert update_model_output["id"] == inc_model_id + assert update_model_output["decommissioned"] is True + + list_models_output = self.cmd( + "dt model list -n {}".format(instance_name) + ).get_output_in_json() + + # Delete non-referenced models first + for model in list_models_output: + if model["id"] != component_id: + self.cmd( + "dt model delete -n {} --dtmi {}".format(instance_name, model["id"]) + ) + + # Now referenced component + self.cmd( + "dt model delete -n {} --dtmi {}".format(instance_name, component_id) + ) + + assert ( + len( + self.cmd( + "dt model list -n {}".format(instance_name) + ).get_output_in_json() + ) + == 0 + ) + + +def assert_create_models_attributes( + result, directory_path=None, models=None, return_metadata=True +): + if not any([directory_path, models]): + raise ValueError("Provide directory_path or models.") + + if directory_path: + local_test_models = _get_models_from_directory(directory_path) + + assert len(result) == len(local_test_models) + + for m in result: + local_model = [model for model in local_test_models if model["id"] == m["id"]] + assert len(local_model) == 1 + assert m["id"] == local_model[0]["id"] + + +def _get_models_from_directory(from_directory): + payload = [] + for entry in scantree(from_directory): + if not entry.name.endswith(".json"): + logger.debug( + "Skipping {} - model file must end with .json".format(entry.path) + ) + continue + entry_json = process_json_arg(content=entry.path, argument_name=entry.name) + payload.append(entry_json) + + return _get_models_metadata(payload) + + +def _get_models_metadata(models): + models_metadata = [] + for model in models: + metadata = { + "id": model["@id"], + "decommissioned": False, + "displayName": model.get("displayName", ""), + "resolveSource": "$devloper", # Currently no other resolveSource + "serviceOrigin": "ADT", # Currently no other serviceOrigin + } + models_metadata.append(metadata) + + return models_metadata + + +def _increment_model_id(model_id): + # This block is to increment model version for + # executing model create of a different style + model_ver = int(model_id.split(";")[-1]) + model_ver = model_ver + 1 + model_id_chars = list(model_id) + model_id_chars[-1] = str(model_ver) + inc_model_id = "".join(model_id_chars) + return inc_model_id diff --git a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py new file mode 100644 index 000000000..f2141a9c3 --- /dev/null +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -0,0 +1,537 @@ +# 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 time import sleep +from knack.log import get_logger +from azext_iot.common.utility import validate_key_value_pairs +from azext_iot.digitaltwins.common import ADTEndpointType +from ..settings import DynamoSettings +from . import DTLiveScenarioTest +from . import ( + MOCK_RESOURCE_TAGS, + MOCK_ENDPOINT_TAGS, + generate_resource_id, + generate_group_id, +) + +logger = get_logger(__name__) + +resource_test_env_vars = [ + "azext_dt_rbac_assignee_owner", + "azext_dt_rbac_assignee_reader", + "azext_dt_ep_eventhub_namespace", + "azext_dt_ep_eventhub_policy", + "azext_dt_ep_eventhub_topic", + "azext_dt_ep_servicebus_namespace", + "azext_dt_ep_servicebus_policy", + "azext_dt_ep_servicebus_topic", + "azext_dt_ep_eventgrid_topic", + "azext_dt_ep_rg" +] + +settings = DynamoSettings([], resource_test_env_vars) +run_rbac_tests = False +run_resource_tests = False +run_endpoint_route_tests = False + +if all( + [ + settings.env.azext_dt_rbac_assignee_owner, + settings.env.azext_dt_rbac_assignee_reader, + ] +): + run_rbac_tests = True + +if all( + [ + settings.env.azext_dt_rbac_assignee_owner, + settings.env.azext_dt_ep_eventhub_namespace, + settings.env.azext_dt_ep_eventhub_policy, + settings.env.azext_dt_ep_eventhub_topic, + settings.env.azext_dt_ep_servicebus_namespace, + settings.env.azext_dt_ep_servicebus_policy, + settings.env.azext_dt_ep_servicebus_topic, + settings.env.azext_dt_ep_eventgrid_topic, + settings.env.azext_dt_ep_rg, + ] +): + run_endpoint_route_tests = True + + +class TestDTResourceLifecycle(DTLiveScenarioTest): + def __init__(self, test_case): + self.group_names = [generate_group_id(), generate_group_id()] + self.instance_names = [generate_resource_id(), generate_resource_id()] + self.dt_location = "westus2" + + super(TestDTResourceLifecycle, self).__init__(test_case, self.group_names) + + def test_dt_resource(self): + create_output = self.cmd( + "dt create -n {} -g {} -l {} --tags {}".format( + self.instance_names[0], + self.group_names[0], + self.dt_location, + MOCK_RESOURCE_TAGS, + ) + ).get_output_in_json() + + assert_common_resource_attributes( + create_output, + self.instance_names[0], + self.group_names[0], + self.dt_location, + MOCK_RESOURCE_TAGS, + ) + + # Explictly assert create prevents provisioning on a name conflict (across regions) + self.cmd( + "dt create -n {} -g {} -l {} --tags {}".format( + self.instance_names[0], + self.group_names[0], + "eastus2euap", + MOCK_RESOURCE_TAGS, + ), + expect_failure=True, + ) + + create_output = self.cmd( + "dt create -n {} -g {} -l {}".format( + self.instance_names[1], self.group_names[1], self.dt_location + ) + ).get_output_in_json() + + assert_common_resource_attributes( + create_output, + self.instance_names[1], + self.group_names[1], + self.dt_location, + None, + ) + + show_output = self.cmd( + "dt show -n {}".format(self.instance_names[0],) + ).get_output_in_json() + + assert_common_resource_attributes( + show_output, + self.instance_names[0], + self.group_names[0], + self.dt_location, + MOCK_RESOURCE_TAGS, + ) + + show_output = self.cmd( + "dt show -n {} -g {}".format(self.instance_names[1], self.group_names[1]) + ).get_output_in_json() + + assert_common_resource_attributes( + show_output, + self.instance_names[1], + self.group_names[1], + self.dt_location, + None, + ) + + list_output = self.cmd("dt list").get_output_in_json() + filtered_list = filter_dt_list(list_output, self.instance_names) + assert len(filtered_list) == len(self.instance_names) + + for group in self.group_names: + list_output = self.cmd("dt list -g {}".format(group)).get_output_in_json() + filtered_group_list = filter_dt_list(list_output, self.instance_names) + assert len(filtered_group_list) == 1 + + # Delete does not currently return output + self.cmd("dt delete -n {}".format(self.instance_names[0])) + self.cmd( + "dt delete -n {} -g {}".format(self.instance_names[1], self.group_names[1]) + ) + + @pytest.mark.skipif( + not run_rbac_tests, + reason="azext_dt_rbac_assignee_owner and azext_dt_rbac_assignee_reader values are required.", + ) + def test_dt_rbac(self): + self.rbac_assignee_owner = settings.env.azext_dt_rbac_assignee_owner + self.rbac_assignee_reader = settings.env.azext_dt_rbac_assignee_reader + + rbac_instance_name = generate_resource_id() + self.cmd( + "dt create -n {} -g {} -l {}".format( + rbac_instance_name, self.group_names[0], self.dt_location, + ) + ) + + assert ( + len( + self.cmd( + "dt role-assignment list -n {}".format(rbac_instance_name) + ).get_output_in_json() + ) + == 0 + ) + + assign_output = self.cmd( + "dt role-assignment create -n {} --assignee {} --role '{}'".format( + rbac_instance_name, self.rbac_assignee_owner, self.role_map["owner"] + ) + ).get_output_in_json() + + assert_common_rbac_attributes( + assign_output, + rbac_instance_name, + "owner", + self.rbac_assignee_owner, + ) + + assign_output = self.cmd( + "dt role-assignment create -n {} --assignee {} --role '{}' -g {}".format( + rbac_instance_name, + self.rbac_assignee_reader, + self.role_map["reader"], + self.group_names[0], + ) + ).get_output_in_json() + + assert_common_rbac_attributes( + assign_output, + rbac_instance_name, + "reader", + self.rbac_assignee_reader, + ) + + # Also grant reader to owner principal for assignment removal test later + assign_output = self.cmd( + "dt role-assignment create -n {} --assignee {} --role '{}'".format( + rbac_instance_name, + self.rbac_assignee_owner, + self.role_map["reader"], + ) + ).get_output_in_json() + + list_assigned_output = self.cmd( + "dt role-assignment list -n {}".format(rbac_instance_name) + ).get_output_in_json() + + assert len(list_assigned_output) == 3 + + # role-assignment delete does not currently return output + + # Remove all role assignments (1) for assignee + self.cmd( + "dt role-assignment delete -n {} --assignee {}".format( + rbac_instance_name, self.rbac_assignee_reader + ) + ) + + # Remove specific role assignment (reader) for assignee + self.cmd( + "dt role-assignment delete -n {} --assignee {} --role '{}'".format( + rbac_instance_name, self.rbac_assignee_owner, self.role_map["reader"], + ) + ) + + list_assigned_output = self.cmd( + "dt role-assignment list -n {} -g {}".format( + rbac_instance_name, self.group_names[0] + ) + ).get_output_in_json() + + assert len(list_assigned_output) == 1 + + self.cmd("dt delete -n {}".format(rbac_instance_name)) + + @pytest.mark.skipif( + not run_endpoint_route_tests, + reason="azext_dt_rbac_assignee_owner and all azext_dt_ep_* env vars are required.", + ) + def test_dt_endpoints_routes(self): + endpoints_instance_name = generate_resource_id() + self.cmd( + "dt create -n {} -g {} -l {}".format( + endpoints_instance_name, self.group_names[0], self.dt_location, + ) + ) + + self.rbac_assignee_owner = settings.env.azext_dt_rbac_assignee_owner + + # Setup RBAC so we can interact with routes + self.cmd( + "dt role-assignment create -n {} --assignee {} --role '{}' -g {}".format( + endpoints_instance_name, + self.rbac_assignee_owner, + self.role_map["owner"], + self.group_names[0], + ) + ) + + sleep(4) # Wait for service to catch-up + + list_ep_output = self.cmd( + "dt endpoint list -n {}".format(endpoints_instance_name) + ).get_output_in_json() + assert len(list_ep_output) == 0 + + eventgrid_rg = settings.env.azext_dt_ep_rg + eventgrid_topic = settings.env.azext_dt_ep_eventgrid_topic + eventgrid_endpoint = "myeventgridendpoint" + + logger.debug("Adding eventgrid endpoint...") + add_ep_output = self.cmd( + "dt endpoint create eventgrid -n {} -g {} --egg {} --egt {} --en {} --tags {}".format( + endpoints_instance_name, + self.group_names[0], + eventgrid_rg, + eventgrid_topic, + eventgrid_endpoint, + MOCK_ENDPOINT_TAGS, + ) + ).get_output_in_json() + + assert_common_endpoint_attributes( + add_ep_output, + eventgrid_endpoint, + ADTEndpointType.eventgridtopic, + MOCK_ENDPOINT_TAGS, + ) + + servicebus_rg = settings.env.azext_dt_ep_rg + servicebus_namespace = settings.env.azext_dt_ep_servicebus_namespace + servicebus_policy = settings.env.azext_dt_ep_servicebus_policy + servicebus_topic = settings.env.azext_dt_ep_servicebus_topic + servicebus_endpoint = "myservicebusendpoint" + + logger.debug("Adding servicebus topic endpoint...") + add_ep_output = self.cmd( + "dt endpoint create servicebus -n {} --sbg {} --sbn {} --sbp {} --sbt {} --en {} --tags {}".format( + endpoints_instance_name, + servicebus_rg, + servicebus_namespace, + servicebus_policy, + servicebus_topic, + servicebus_endpoint, + MOCK_ENDPOINT_TAGS, + ) + ).get_output_in_json() + + assert_common_endpoint_attributes( + add_ep_output, + servicebus_endpoint, + ADTEndpointType.servicebus, + MOCK_ENDPOINT_TAGS, + ) + + eventhub_rg = settings.env.azext_dt_ep_rg + eventhub_namespace = settings.env.azext_dt_ep_eventhub_namespace + eventhub_policy = settings.env.azext_dt_ep_eventhub_policy + eventhub_topic = settings.env.azext_dt_ep_eventhub_topic + eventhub_endpoint = "myeventhubendpoint" + + logger.debug("Adding eventhub endpoint...") + add_ep_output = self.cmd( + "dt endpoint create eventhub -n {} --ehg {} --ehn {} --ehp {} --eh {} --en {}".format( + endpoints_instance_name, + eventhub_rg, + eventhub_namespace, + eventhub_policy, + eventhub_topic, + eventhub_endpoint, + ) + ).get_output_in_json() + + assert_common_endpoint_attributes( + add_ep_output, eventhub_endpoint, ADTEndpointType.eventhub, None, + ) + + show_ep_output = self.cmd( + "dt endpoint show -n {} --en {}".format( + endpoints_instance_name, eventhub_endpoint, + ) + ).get_output_in_json() + + assert_common_endpoint_attributes( + show_ep_output, eventhub_endpoint, ADTEndpointType.eventhub, None, + ) + + show_ep_output = self.cmd( + "dt endpoint show -n {} -g {} --en {}".format( + endpoints_instance_name, self.group_names[0], servicebus_endpoint, + ) + ).get_output_in_json() + + assert_common_endpoint_attributes( + show_ep_output, + servicebus_endpoint, + ADTEndpointType.servicebus, + MOCK_ENDPOINT_TAGS, + ) + + list_ep_output = self.cmd( + "dt endpoint list -n {} -g {}".format( + endpoints_instance_name, self.group_names[0] + ) + ).get_output_in_json() + assert len(list_ep_output) == 3 + + endpoint_names = [eventgrid_endpoint, servicebus_endpoint, eventhub_endpoint] + filter_values = ["", "false", "type = Microsoft.DigitalTwins.Twin.Create"] + + # Test Routes + list_routes_output = self.cmd( + "dt route list -n {}".format(endpoints_instance_name) + ).get_output_in_json() + assert len(list_routes_output) == 0 + + for endpoint_name in endpoint_names: + is_last = endpoint_name == endpoint_names[-1] + route_name = "routefor{}".format(endpoint_name) + filter_value = filter_values.pop() + add_route_output = self.cmd( + "dt route create -n {} --rn {} --en {} --filter '{}' {}".format( + endpoints_instance_name, + route_name, + endpoint_name, + filter_value, + "-g {}".format(self.group_names[0]) if is_last else "", + ) + ).get_output_in_json() + + assert_common_route_attributes( + add_route_output, route_name, endpoint_name, filter_value + ) + + show_route_output = self.cmd( + "dt route show -n {} --rn {} {}".format( + endpoints_instance_name, + route_name, + "-g {}".format(self.group_names[0]) if is_last else "", + ) + ).get_output_in_json() + + assert_common_route_attributes( + show_route_output, route_name, endpoint_name, filter_value + ) + + list_routes_output = self.cmd( + "dt route list -n {} -g {}".format( + endpoints_instance_name, self.group_names[0] + ) + ).get_output_in_json() + assert len(list_routes_output) == 3 + + for endpoint_name in endpoint_names: + is_last = endpoint_name == endpoint_names[-1] + route_name = "routefor{}".format(endpoint_name) + self.cmd( + "dt route delete -n {} --rn {} {}".format( + endpoints_instance_name, + route_name, + "-g {}".format(self.group_names[0]) if is_last else "", + ) + ) + + list_routes_output = self.cmd( + "dt route list -n {} -g {}".format( + endpoints_instance_name, self.group_names[0] + ) + ).get_output_in_json() + assert len(list_routes_output) == 0 + + # Unfortuntely the service does not yet know how to delete child resouces + # of a dt parent automatically. So we have to explictly delete every endpoint first. + + for endpoint_name in endpoint_names: + logger.debug("Cleaning up {} endpoint...".format(endpoint_name)) + is_last = endpoint_name == endpoint_names[-1] + delete_ep_output = self.cmd( + "dt endpoint delete -n {} --en {} {}".format( + endpoints_instance_name, + endpoint_name, + "-g {}".format(self.group_names[0]) if is_last else "", + ) + ).get_output_in_json() + assert delete_ep_output["provisioningState"] == "Deleting" + assert delete_ep_output["id"].endswith("/{}".format(endpoint_name)) + sleep(15) # Wait for service to catch-up. Service will fix at some point. + + self.cmd( + "dt delete -n {} -g {}".format(endpoints_instance_name, self.group_names[0]) + ) + + +def assert_common_resource_attributes( + instance_output, resource_id, group_id, location, tags +): + assert instance_output["createdTime"] + assert instance_output["hostName"].startswith(resource_id) + assert instance_output["location"] == location + assert instance_output["id"].endswith(resource_id) + assert instance_output["lastUpdatedTime"] + assert instance_output["name"] == resource_id + assert instance_output["provisioningState"] == "Succeeded" + assert instance_output["resourceGroup"] == group_id + assert instance_output["type"] == "Microsoft.DigitalTwins/digitalTwinsInstances" + assert instance_output["tags"] == validate_key_value_pairs(tags) + + +def assert_common_route_attributes( + route_output, route_name, endpoint_name, filter_value +): + assert route_output["endpointName"] == endpoint_name + assert route_output["id"] == route_name + assert route_output["filter"] == filter_value if filter_value else "true" + + +def assert_common_endpoint_attributes( + endpoint_output, endpoint_name, endpoint_type, tags +): + assert endpoint_output["id"].endswith("/{}".format(endpoint_name)) + assert ( + endpoint_output["type"] + == "Microsoft.DigitalTwins/digitalTwinsInstances/endpoints" + ) + assert endpoint_output["resourceGroup"] + + if tags: + assert endpoint_output["properties"]["tags"] == validate_key_value_pairs(tags) + + assert endpoint_output["properties"]["provisioningState"] + assert endpoint_output["properties"]["createdTime"] + + if endpoint_type == ADTEndpointType.eventgridtopic: + assert endpoint_output["properties"]["TopicEndpoint"] + assert endpoint_output["properties"]["accessKey1"] + assert endpoint_output["properties"]["accessKey2"] + assert endpoint_output["properties"]["endpointType"] == "EventGrid" + return + if endpoint_type == ADTEndpointType.servicebus: + assert endpoint_output["properties"]["primaryConnectionString"] + assert endpoint_output["properties"]["secondaryConnectionString"] + assert endpoint_output["properties"]["endpointType"] == "ServiceBus" + return + if endpoint_type == ADTEndpointType.eventhub: + assert endpoint_output["properties"]["connectionString-PrimaryKey"] + assert endpoint_output["properties"]["connectionString-SecondaryKey"] + assert endpoint_output["properties"]["endpointType"] == "EventHub" + return + + +def assert_common_rbac_attributes(rbac_output, instance_name, role_name, assignee): + role_def_id = None + if role_name == "owner": + role_def_id = "/bcd981a7-7f74-457b-83e1-cceb9e632ffe" + elif role_name == "reader": + role_def_id = "/d57506d4-4c8d-48b1-8587-93c323f6a5a3" + + assert rbac_output["roleDefinitionId"].endswith(role_def_id) + assert rbac_output["type"] == "Microsoft.Authorization/roleAssignments" + assert rbac_output["scope"].endswith("/{}".format(instance_name)) + + +def filter_dt_list(list_output, valid_names): + return [inst for inst in list_output if inst["name"] in valid_names] diff --git a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py new file mode 100644 index 000000000..7a86628f1 --- /dev/null +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -0,0 +1,410 @@ +# 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 +import json +from time import sleep +from knack.log import get_logger +from ..settings import DynamoSettings +from . import DTLiveScenarioTest +from . import ( + generate_resource_id, + generate_group_id, +) + +logger = get_logger(__name__) + +twin_tests_env_vars = [ + "azext_dt_rbac_assignee_owner", +] + +settings = DynamoSettings([], twin_tests_env_vars) +run_tests = False + +if all([settings.env.azext_dt_rbac_assignee_owner]): + run_tests = True + + +@pytest.mark.skipif(not run_tests, reason="azext_dt_rbac_assignee_owner is required.") +@pytest.mark.usefixtures("set_cwd") +class TestDTTwinLifecycle(DTLiveScenarioTest): + def __init__(self, test_case): + self.group_names = [generate_group_id()] + self.instance_names = [generate_resource_id(), generate_resource_id()] + self.rbac_assignee_owner = settings.env.azext_dt_rbac_assignee_owner + self.dt_location = "westus2" + self.instance_name = generate_resource_id() + self.floor_dtmi = "dtmi:example:Floor;1" + self.floor_twin_id = "myfloor" + self.room_dtmi = "dtmi:example:Room;1" + self.room_twin_id = "myroom" + self.thermostat_dtmi = "dtmi:com:example:Thermostat;1" + self.thermostat_component_id = "Thermostat" + + super(TestDTTwinLifecycle, self).__init__(test_case, self.group_names) + + def test_dt_twin(self): + models_directory = "./models" + + self.cmd( + "dt create -n {} -g {} -l {}".format( + self.instance_name, self.group_names[0], self.dt_location + ) + ) + + self.cmd( + "dt role-assignment create -n {} -g {} --assignee {} --role '{}'".format( + self.instance_name, self.group_names[0], self.rbac_assignee_owner, self.role_map["owner"] + ) + ) + # Wait for RBAC to catch-up + sleep(10) + + self.cmd( + "dt model create -n {} --from-directory '{}'".format( + self.instance_name, models_directory + ) + ) + + twin_query_result = self.cmd( + "dt twin query -n {} -q 'select * from digitaltwins'".format( + self.instance_name + ) + ).get_output_in_json() + assert len(twin_query_result["result"]) == 0 + + self.kwargs["tempAndThermostatComponentJson"] = json.dumps( + { + "Temperature": 10.2, + "Thermostat": { + "$metadata": {"$model": self.thermostat_dtmi}, + "setPointTemp": 23.12, + }, + } + ) + + floor_twin = self.cmd( + "dt twin create -n {} --dtmi {} --twin-id {}".format( + self.instance_name, self.floor_dtmi, self.floor_twin_id + ) + ).get_output_in_json() + + assert_twin_attributes( + twin=floor_twin, + expected_twin_id=self.floor_twin_id, + expected_dtmi=self.floor_dtmi, + ) + + room_twin = self.cmd( + "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( + self.instance_name, + self.group_names[0], + self.room_dtmi, + self.room_twin_id, + "{tempAndThermostatComponentJson}", + ) + ).get_output_in_json() + + assert_twin_attributes( + twin=room_twin, + expected_twin_id=self.room_twin_id, + expected_dtmi=self.room_dtmi, + properties=self.kwargs["tempAndThermostatComponentJson"], + component_name=self.thermostat_component_id, + ) + + # Component + + thermostat_component = self.cmd( + "dt twin component show -n {} -g {} --twin-id {} --component {}".format( + self.instance_name, + self.group_names[0], + self.room_twin_id, + self.thermostat_component_id, + ) + ).get_output_in_json() + + assert thermostat_component["$metadata"]["$model"] == self.thermostat_dtmi + + self.kwargs["thermostatJsonPatch"] = json.dumps( + [{"op": "replace", "path": "/setPointTemp", "value": 50.5}] + ) + + # Currently component update does not return value + self.cmd( + "dt twin component update -n {} -g {} --twin-id {} --component {} --json-patch '{}'".format( + self.instance_name, + self.group_names[0], + self.room_twin_id, + self.thermostat_component_id, + "{thermostatJsonPatch}", + ) + ) + + thermostat_component = self.cmd( + "dt twin component show -n {} -g {} --twin-id {} --component {}".format( + self.instance_name, + self.group_names[0], + self.room_twin_id, + self.thermostat_component_id, + ) + ).get_output_in_json() + + assert ( + thermostat_component["setPointTemp"] + == json.loads(self.kwargs["thermostatJsonPatch"])[0]["value"] + ) + + twins_id_list = [ + (self.floor_twin_id, self.floor_dtmi), + (self.room_twin_id, self.room_dtmi), + ] + + for twin_tuple in twins_id_list: + twin = self.cmd( + "dt twin show -n {} --twin-id {} {}".format( + self.instance_name, + twin_tuple[0], + "-g {}".format(self.group_names[0]) + if twins_id_list[-1] == twin_tuple + else "", + ) + ).get_output_in_json() + assert_twin_attributes( + twin=twin, expected_twin_id=twin_tuple[0], expected_dtmi=twin_tuple[1] + ) + + self.kwargs["temperatureJsonPatch"] = json.dumps( + {"op": "replace", "path": "/Temperature", "value": 20.2} + ) + + update_twin_result = self.cmd( + "dt twin update -n {} --twin-id {} --json-patch '{}'".format( + self.instance_name, self.room_twin_id, "{temperatureJsonPatch}", + ) + ).get_output_in_json() + + assert ( + update_twin_result["Temperature"] + == json.loads(self.kwargs["temperatureJsonPatch"])["value"] + ) + + twin_query_result = self.cmd( + "dt twin query -n {} -g {} -q 'select * from digitaltwins'".format( + self.instance_name, self.group_names[0] + ) + ).get_output_in_json() + assert len(twin_query_result["result"]) == 2 + + relationship_id = "myedge" + relationship = "contains" + self.kwargs["relationshipJson"] = json.dumps( + {"ownershipUser": "me", "ownershipDepartment": "mydepartment"} + ) + self.kwargs["relationshipJsonPatch"] = json.dumps( + {"op": "replace", "path": "/ownershipUser", "value": "meme"} + ) + + twin_relationship_create_result = self.cmd( + "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --source-twin-id {} " + "--target-twin-id {} --properties '{}'".format( + self.instance_name, + self.group_names[0], + relationship_id, + relationship, + self.floor_twin_id, + self.room_twin_id, + "{relationshipJson}", + ) + ).get_output_in_json() + + assert_twin_relationship_attributes( + twin_relationship_obj=twin_relationship_create_result, + expected_relationship=relationship, + relationship_id=relationship_id, + source_id=self.floor_twin_id, + target_id=self.room_twin_id, + properties=self.kwargs["relationshipJson"], + ) + + twin_relationship_show_result = self.cmd( + "dt twin relationship show -n {} -g {} --twin-id {} --relationship-id {}".format( + self.instance_name, + self.group_names[0], + self.floor_twin_id, + relationship_id, + ) + ).get_output_in_json() + + assert_twin_relationship_attributes( + twin_relationship_obj=twin_relationship_show_result, + expected_relationship=relationship, + relationship_id=relationship_id, + source_id=self.floor_twin_id, + target_id=self.room_twin_id, + properties=self.kwargs["relationshipJson"], + ) + + twin_edge_update_result = self.cmd( + "dt twin relationship update -n {} -g {} --relationship-id {} --twin-id {} " + "--json-patch '{}'".format( + self.instance_name, + self.group_names[0], + relationship_id, + self.floor_twin_id, + "{relationshipJsonPatch}", + ) + ).get_output_in_json() + + assert ( + twin_edge_update_result["ownershipUser"] + == json.loads(self.kwargs["relationshipJsonPatch"])["value"] + ) + + twin_relationship_list_result = self.cmd( + "dt twin relationship list -n {} --twin-id {}".format( + self.instance_name, self.floor_twin_id, + ) + ).get_output_in_json() + assert len(twin_relationship_list_result) == 1 + + twin_relationship_list_result = self.cmd( + "dt twin relationship list -n {} -g {} --twin-id {} --relationship {}".format( + self.instance_name, + self.group_names[0], + self.floor_twin_id, + relationship, + ) + ).get_output_in_json() + assert len(twin_relationship_list_result) == 1 + + twin_relationship_list_result = self.cmd( + "dt twin relationship list -n {} --twin-id {}".format( + self.instance_name, self.room_twin_id, + ) + ).get_output_in_json() + assert len(twin_relationship_list_result) == 0 + + twin_relationship_list_result = self.cmd( + "dt twin relationship list -n {} --twin-id {} --incoming".format( + self.instance_name, self.room_twin_id, + ) + ).get_output_in_json() + assert len(twin_relationship_list_result) == 1 + + twin_relationship_list_result = self.cmd( + "dt twin relationship list -n {} --twin-id {} --kind {} --incoming".format( + self.instance_name, self.room_twin_id, relationship + ) + ).get_output_in_json() + assert len(twin_relationship_list_result) == 1 + + # No output from API for delete edge + self.cmd( + "dt twin relationship delete -n {} --twin-id {} -r {}".format( + self.instance_name, self.floor_twin_id, relationship_id, + ) + ) + + twin_relationship_list_result = self.cmd( + "dt twin relationship list -n {} -g {} --twin-id {} --kind {}".format( + self.instance_name, + self.group_names[0], + self.floor_twin_id, + relationship, + ) + ).get_output_in_json() + assert len(twin_relationship_list_result) == 0 + + # Twin + Component Telemetry. Neither returns data. Only 204 status code. + + self.kwargs["telemetryJson"] = json.dumps({"data": generate_resource_id()}) + + self.cmd( + "dt twin telemetry send -n {} -g {} --twin-id {} --telemetry '{}'".format( + self.instance_name, + self.group_names[0], + self.room_twin_id, + "{telemetryJson}", + ) + ) + + self.cmd( + "dt twin telemetry send -n {} -g {} --twin-id {} --component {} --telemetry '{}'".format( + self.instance_name, + self.group_names[0], + self.room_twin_id, + self.thermostat_component_id, + "{telemetryJson}", + ) + ) + + for twin_tuple in twins_id_list: + # No output from API for delete twin + self.cmd( + "dt twin delete -n {} --twin-id {} {}".format( + self.instance_name, + twin_tuple[0], + "-g {}".format(self.group_names[0]) + if twins_id_list[-1] == twin_tuple + else "", + ) + ) + sleep(4) # Wait for API to catch up + twin_query_result = self.cmd( + "dt twin query -n {} -g {} -q 'select * from digitaltwins' --cost".format( + self.instance_name, self.group_names[0] + ) + ).get_output_in_json() + assert len(twin_query_result["result"]) == 0 + assert twin_query_result["cost"] + + +# TODO: Refactor - limited interface +def assert_twin_attributes( + twin, expected_twin_id, expected_dtmi, properties=None, component_name=None +): + assert twin["$dtId"] == expected_twin_id + assert twin["$etag"] + + metadata = twin["$metadata"] + metadata["$model"] == expected_dtmi + + if properties: + properties = json.loads(properties) + + for key in properties: + if key != component_name: + assert metadata[key]["desiredValue"] == properties[key] + + if component_name: + component_metadata = twin[component_name]["$metadata"] + component_props = properties[component_name] + + for key in component_props: + if key != "$metadata": + assert ( + component_metadata[key]["desiredValue"] == component_props[key] + ) + + +def assert_twin_relationship_attributes( + twin_relationship_obj, + expected_relationship, + relationship_id, + source_id, + target_id, + properties=None, +): + assert twin_relationship_obj["$relationshipId"] == relationship_id + assert twin_relationship_obj["$relationshipName"] == expected_relationship + assert twin_relationship_obj["$sourceId"] == source_id + assert twin_relationship_obj["$targetId"] == target_id + + if properties: + properties = json.loads(properties) + for key in properties: + assert twin_relationship_obj[key] == properties[key] diff --git a/azext_iot/tests/settings.py b/azext_iot/tests/settings.py index 3b03560c1..de665c825 100644 --- a/azext_iot/tests/settings.py +++ b/azext_iot/tests/settings.py @@ -7,7 +7,11 @@ from os import environ -ENV_SET_TEST_IOTHUB_BASIC = ["azext_iot_testhub", "azext_iot_testrg", "azext_iot_testhub_cs"] +ENV_SET_TEST_IOTHUB_BASIC = [ + "azext_iot_testhub", + "azext_iot_testrg", + "azext_iot_testhub_cs", +] class Setting(object): @@ -34,5 +38,7 @@ def _build_config(self, env_set, optional=False): value = environ.get(key) if not value: if not optional: - raise RuntimeError("'{}' environment variable required.".format(key)) + raise RuntimeError( + "'{}' environment variable required.".format(key) + ) setattr(self.env, key, value) diff --git a/dev_requirements b/dev_requirements index d6e877036..09395fa5d 100644 --- a/dev_requirements +++ b/dev_requirements @@ -5,7 +5,7 @@ pytest-env pylint uamqp~=1.2 mock;python_version<'3.3' -responses +responses==0.10.12 flake8 black;python_version>='3.6' wheel==0.30.0 diff --git a/pytest.ini.example b/pytest.ini.example index bad0c38df..6d8bd7e0c 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -25,3 +25,13 @@ env = azext_iot_central_app_id= azext_iot_central_device_id= azext_iot_central_device_template_path=./azext_iot/tests/central/json/device_template_int_test.json + azext_dt_rbac_assignee_owner= + azext_dt_rbac_assignee_reader= + azext_dt_ep_eventgrid_topic= + azext_dt_ep_servicebus_namespace= + azext_dt_ep_servicebus_policy= + azext_dt_ep_servicebus_topic= + azext_dt_ep_eventhub_namespace= + azext_dt_ep_eventhub_policy= + azext_dt_ep_eventhub_topic= + azext_dt_ep_rg= From addfd3460e0bc07d525b0cd87226d8ce8654da04 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 5 Jun 2020 13:27:51 -0700 Subject: [PATCH 045/179] ADT clean up (#196) * Primarily non-functional format changes. Removed unneeded enums. Added table transformers for the resources that need them. --- HISTORY.rst | 48 +++-- azext_iot/common/utility.py | 3 +- azext_iot/digitaltwins/_help.py | 6 +- azext_iot/digitaltwins/command_map.py | 58 ++++-- azext_iot/digitaltwins/commands_models.py | 4 +- azext_iot/digitaltwins/commands_rbac.py | 7 +- azext_iot/digitaltwins/commands_resource.py | 198 +++++++++++++------- azext_iot/digitaltwins/commands_routes.py | 8 +- azext_iot/digitaltwins/common.py | 18 -- azext_iot/digitaltwins/params.py | 10 +- azext_iot/digitaltwins/providers/auth.py | 15 +- azext_iot/digitaltwins/providers/generic.py | 10 +- azext_iot/digitaltwins/providers/rbac.py | 7 +- azext_iot/digitaltwins/providers/route.py | 11 +- dev_requirements | 6 +- 15 files changed, 246 insertions(+), 163 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 103efd8a5..b61c97785 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,31 +3,43 @@ Release History =============== +0.9.4 ++++++++++++++++ +* Azure Digital Twins Public Preview - CLI release + * Introducing 35 new commands in the following command groups + * az dt + * az dt endpoint + * az dt model + * az dt role-assignment + * az dt route + * az dt twin + * az dt twin relationship + * az dt twin telemety + 0.9.3 +++++++++++++++ - * IoT Hub device identity import/export commands support usage via managed service identity using the --auth-type argument. - * Adds preview command group "az iot central app device" - * Adds preview command "az iot central app device create" - * Adds preview command "az iot central app device show" - * Adds preview command "az iot central app device list" - * Adds preview command "az iot central app device delete" - * Adds preview command "az iot central app device registration-info" - * Adds preview command "az iot central app device registration-summary" - * Adds preview command group "az iot central app device-template" - * Adds preview command "az iot central app device-template create" - * Adds preview command "az iot central app device-template show" - * Adds preview command "az iot central app device-template list" - * Adds preview command "az iot central app device-template delete" - * Adds preview command "az iot central app device-template map" - * Changed how results are displayed in "az iot central app validate-messages" +* IoT Hub device identity import/export commands support usage via managed service identity using the --auth-type argument. +* Adds preview command group "az iot central app device" + * Adds preview command "az iot central app device create" + * Adds preview command "az iot central app device show" + * Adds preview command "az iot central app device list" + * Adds preview command "az iot central app device delete" + * Adds preview command "az iot central app device registration-info" + * Adds preview command "az iot central app device registration-summary" +* Adds preview command group "az iot central app device-template" + * Adds preview command "az iot central app device-template create" + * Adds preview command "az iot central app device-template show" + * Adds preview command "az iot central app device-template list" + * Adds preview command "az iot central app device-template delete" + * Adds preview command "az iot central app device-template map" +* Changed how results are displayed in "az iot central app validate-messages" * Known issues: * The following preview commands will retrieve at most 25 results - * az iot central app device list - * az iot central app device-template list + * az iot central app device list + * az iot central app device-template list * az iot central app device-template map - 0.9.2 +++++++++++++++ * Device and module twin update operations provide explicit patch arguments (--desired, --tags). diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index a3c3b114a..f93fbd7bb 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -176,7 +176,7 @@ def read_file_content(file_path, allow_binary=False): if allow_binary: try: - with open(file_path, 'rb') as input_file: + with open(file_path, "rb") as input_file: logger.debug("Attempting to read file %s as binary", file_path) return base64.b64encode(input_file.read()).decode("utf-8") except Exception: # pylint: disable=broad-except @@ -466,6 +466,7 @@ def is_iso8601_time(self, to_validate: str) -> bool: def ensure_min_version(cur_ver, min_ver): from pkg_resources._vendor.packaging import version + return version.parse(cur_ver) >= version.parse(min_ver) diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index 139f5bb6f..1a0657040 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -182,13 +182,13 @@ def load_digitaltwins_help(): such as Owner or User Access Administrator at the assigned scope. examples: - - name: Assign a user the built-in Digital Twins Owner role against a target instance. + - name: Assign a user (by email) the built-in Digital Twins Owner role against a target instance. text: > az dt role-assignment create -n {instance_name} --assignee "owneruser@microsoft.com" --role "Azure Digital Twins Owner (Preview)" - - name: Assign a user the built-in Digital Twins Reader role against a target instance. + - name: Assign a user (by object Id) the built-in Digital Twins Reader role against a target instance. text: > - az dt role-assignment create -n {instance_name} --assignee "readeruser@microsoft.com" --role "Azure Digital Twins Reader (Preview)" + az dt role-assignment create -n {instance_name} --assignee "97a89267-0966-4054-a156-b7d86ef8e216" --role "Azure Digital Twins Reader (Preview)" - name: Assign a service principal a custom role against a target instance. text: > diff --git a/azext_iot/digitaltwins/command_map.py b/azext_iot/digitaltwins/command_map.py index 85c04c8bf..ced47d90f 100644 --- a/azext_iot/digitaltwins/command_map.py +++ b/azext_iot/digitaltwins/command_map.py @@ -48,41 +48,57 @@ def load_digitaltwins_commands(self, _): cmd_group.command("delete", "delete_instance") with self.command_group( - "dt endpoint", - command_type=digitaltwins_resource_ops + "dt endpoint", command_type=digitaltwins_resource_ops ) as cmd_group: - cmd_group.command("show", "show_endpoint") - cmd_group.command("list", "list_endpoints") + cmd_group.command( + "show", + "show_endpoint", + table_transformer=( + "{EndpointName:name, EndpointType:properties.endpointType," + "ProvisioningState:properties.provisioningState,CreatedTime:properties.createdTime}" + ), + ) + cmd_group.command( + "list", + "list_endpoints", + table_transformer=( + "[*].{EndpointName:name, EndpointType:properties.endpointType," + "ProvisioningState:properties.provisioningState,CreatedTime:properties.createdTime}" + ), + ) cmd_group.command("delete", "delete_endpoint") with self.command_group( - "dt endpoint create", - command_type=digitaltwins_resource_ops + "dt endpoint create", command_type=digitaltwins_resource_ops ) as cmd_group: cmd_group.command("eventgrid", "add_endpoint_eventgrid") cmd_group.command("servicebus", "add_endpoint_servicebus") cmd_group.command("eventhub", "add_endpoint_eventhub") with self.command_group( - "dt route", - command_type=digitaltwins_route_ops + "dt route", command_type=digitaltwins_route_ops ) as cmd_group: - cmd_group.command("show", "show_route") - cmd_group.command("list", "list_routes") + cmd_group.command( + "show", + "show_route", + table_transformer="{RouteName:id,EndpointName:endpointName,Filter:filter}", + ) + cmd_group.command( + "list", + "list_routes", + table_transformer="[*].{RouteName:id,EndpointName:endpointName,Filter:filter}", + ) cmd_group.command("delete", "delete_route") cmd_group.command("create", "create_route") with self.command_group( - "dt role-assignment", - command_type=digitaltwins_rbac_ops + "dt role-assignment", command_type=digitaltwins_rbac_ops ) as cmd_group: cmd_group.command("create", "assign_role") cmd_group.command("delete", "remove_role") cmd_group.command("list", "list_assignments") - with self.command_group( - "dt twin", command_type=digitaltwins_twin_ops - ) as cmd_group: + with self.command_group("dt twin", command_type=digitaltwins_twin_ops) as cmd_group: cmd_group.command("query", "query_twins") cmd_group.command("create", "create_twin") cmd_group.command("show", "show_twin") @@ -113,7 +129,15 @@ def load_digitaltwins_commands(self, _): "dt model", command_type=digitaltwins_model_ops ) as cmd_group: cmd_group.command("create", "add_models") - cmd_group.command("show", "show_model") - cmd_group.command("list", "list_models") + cmd_group.command( + "show", + "show_model", + table_transformer="{ModelId:id,UploadTime:uploadTime,Decommissioned:decommissioned}", + ) + cmd_group.command( + "list", + "list_models", + table_transformer="[*].{ModelId:id,UploadTime:uploadTime,Decommissioned:decommissioned}", + ) cmd_group.command("update", "update_model") cmd_group.command("delete", "delete_model") diff --git a/azext_iot/digitaltwins/commands_models.py b/azext_iot/digitaltwins/commands_models.py index 91f32b766..71e938e0a 100644 --- a/azext_iot/digitaltwins/commands_models.py +++ b/azext_iot/digitaltwins/commands_models.py @@ -36,9 +36,7 @@ def update_model(cmd, name, model_id, decommission=None, resource_group_name=Non return model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) - return model_provider.update( - id=model_id, decommission=decommission, - ) + return model_provider.update(id=model_id, decommission=decommission,) def delete_model(cmd, name, model_id, resource_group_name=None): diff --git a/azext_iot/digitaltwins/commands_rbac.py b/azext_iot/digitaltwins/commands_rbac.py index 98526b882..bf08b4356 100644 --- a/azext_iot/digitaltwins/commands_rbac.py +++ b/azext_iot/digitaltwins/commands_rbac.py @@ -30,10 +30,13 @@ def remove_role(cmd, name, assignee=None, role_type=None, resource_group_name=No ) -def list_assignments(cmd, name, include_inherited=False, role_type=None, resource_group_name=None): +def list_assignments( + cmd, name, include_inherited=False, role_type=None, resource_group_name=None +): rp = ResourceProvider(cmd) return rp.get_role_assignments( name=name, include_inherited=include_inherited, resource_group_name=resource_group_name, - role_type=role_type) + role_type=role_type, + ) diff --git a/azext_iot/digitaltwins/commands_resource.py b/azext_iot/digitaltwins/commands_resource.py index 04f7f1784..40d49add0 100644 --- a/azext_iot/digitaltwins/commands_resource.py +++ b/azext_iot/digitaltwins/commands_resource.py @@ -13,7 +13,9 @@ def create_instance(cmd, name, resource_group_name, location, tags=None): rp = ResourceProvider(cmd) - return rp.create(name=name, resource_group_name=resource_group_name, location=location, tags=tags) + return rp.create( + name=name, resource_group_name=resource_group_name, location=location, tags=tags + ) def list_instances(cmd, resource_group_name=None): @@ -41,21 +43,27 @@ def list_endpoints(cmd, name, resource_group_name=None): def show_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) + return rp.get_endpoint( + name=name, endpoint_name=endpoint_name, resource_group_name=resource_group_name + ) def delete_endpoint(cmd, name, endpoint_name, resource_group_name=None): rp = ResourceProvider(cmd) - return rp.delete_endpoint(name=name, - endpoint_name=endpoint_name, - resource_group_name=resource_group_name) - - -def add_endpoint_eventgrid(cmd, name, endpoint_name, eventgrid_topic_name, - eventgrid_resource_group, resource_group_name=None, - tags=None): + return rp.delete_endpoint( + name=name, endpoint_name=endpoint_name, resource_group_name=resource_group_name + ) + + +def add_endpoint_eventgrid( + cmd, + name, + endpoint_name, + eventgrid_topic_name, + eventgrid_resource_group, + resource_group_name=None, + tags=None, +): return _add_endpoint_eventgrid( cmd=cmd, name=name, @@ -63,27 +71,44 @@ def add_endpoint_eventgrid(cmd, name, endpoint_name, eventgrid_topic_name, eventgrid_resource_group=eventgrid_resource_group, eventgrid_topic_name=eventgrid_topic_name, resource_group_name=resource_group_name, - tags=tags) - - -def _add_endpoint_eventgrid(cmd, name, endpoint_name, eventgrid_topic_name, - eventgrid_resource_group, timeout=15, resource_group_name=None, - tags=None): + tags=tags, + ) + + +def _add_endpoint_eventgrid( + cmd, + name, + endpoint_name, + eventgrid_topic_name, + eventgrid_resource_group, + timeout=15, + resource_group_name=None, + tags=None, +): rp = ResourceProvider(cmd) - return rp.add_endpoint(name=name, - resource_group_name=resource_group_name, - endpoint_name=endpoint_name, - endpoint_resource_type=ADTEndpointType.eventgridtopic, - endpoint_resource_name=eventgrid_topic_name, - endpoint_resource_group=eventgrid_resource_group, - tags=tags, - timeout=timeout) - - -def add_endpoint_servicebus(cmd, name, endpoint_name, servicebus_topic_name, - servicebus_resource_group, servicebus_policy, - servicebus_namespace, resource_group_name=None, - tags=None): + return rp.add_endpoint( + name=name, + resource_group_name=resource_group_name, + endpoint_name=endpoint_name, + endpoint_resource_type=ADTEndpointType.eventgridtopic, + endpoint_resource_name=eventgrid_topic_name, + endpoint_resource_group=eventgrid_resource_group, + tags=tags, + timeout=timeout, + ) + + +def add_endpoint_servicebus( + cmd, + name, + endpoint_name, + servicebus_topic_name, + servicebus_resource_group, + servicebus_policy, + servicebus_namespace, + resource_group_name=None, + tags=None, +): return _add_endpoint_servicebus( cmd=cmd, name=name, @@ -93,30 +118,48 @@ def add_endpoint_servicebus(cmd, name, endpoint_name, servicebus_topic_name, servicebus_policy=servicebus_policy, servicebus_namespace=servicebus_namespace, resource_group_name=resource_group_name, - tags=tags) - - -def _add_endpoint_servicebus(cmd, name, endpoint_name, servicebus_topic_name, - servicebus_resource_group, servicebus_policy, - servicebus_namespace, timeout=15, resource_group_name=None, - tags=None): + tags=tags, + ) + + +def _add_endpoint_servicebus( + cmd, + name, + endpoint_name, + servicebus_topic_name, + servicebus_resource_group, + servicebus_policy, + servicebus_namespace, + timeout=15, + resource_group_name=None, + tags=None, +): rp = ResourceProvider(cmd) - return rp.add_endpoint(name=name, - resource_group_name=resource_group_name, - endpoint_name=endpoint_name, - endpoint_resource_type=ADTEndpointType.servicebus, - endpoint_resource_name=servicebus_topic_name, - endpoint_resource_group=servicebus_resource_group, - endpoint_resource_namespace=servicebus_namespace, - endpoint_resource_policy=servicebus_policy, - tags=tags, - timeout=timeout) - - -def add_endpoint_eventhub(cmd, name, endpoint_name, eventhub_name, - eventhub_resource_group, eventhub_policy, - eventhub_namespace, resource_group_name=None, - tags=None): + return rp.add_endpoint( + name=name, + resource_group_name=resource_group_name, + endpoint_name=endpoint_name, + endpoint_resource_type=ADTEndpointType.servicebus, + endpoint_resource_name=servicebus_topic_name, + endpoint_resource_group=servicebus_resource_group, + endpoint_resource_namespace=servicebus_namespace, + endpoint_resource_policy=servicebus_policy, + tags=tags, + timeout=timeout, + ) + + +def add_endpoint_eventhub( + cmd, + name, + endpoint_name, + eventhub_name, + eventhub_resource_group, + eventhub_policy, + eventhub_namespace, + resource_group_name=None, + tags=None, +): return _add_endpoint_eventhub( cmd=cmd, name=name, @@ -126,21 +169,32 @@ def add_endpoint_eventhub(cmd, name, endpoint_name, eventhub_name, eventhub_policy=eventhub_policy, eventhub_namespace=eventhub_namespace, resource_group_name=resource_group_name, - tags=tags) - - -def _add_endpoint_eventhub(cmd, name, endpoint_name, eventhub_name, - eventhub_resource_group, eventhub_policy, - eventhub_namespace, timeout=15, resource_group_name=None, - tags=None): + tags=tags, + ) + + +def _add_endpoint_eventhub( + cmd, + name, + endpoint_name, + eventhub_name, + eventhub_resource_group, + eventhub_policy, + eventhub_namespace, + timeout=15, + resource_group_name=None, + tags=None, +): rp = ResourceProvider(cmd) - return rp.add_endpoint(name=name, - resource_group_name=resource_group_name, - endpoint_name=endpoint_name, - endpoint_resource_type=ADTEndpointType.eventhub, - endpoint_resource_name=eventhub_name, - endpoint_resource_group=eventhub_resource_group, - endpoint_resource_namespace=eventhub_namespace, - endpoint_resource_policy=eventhub_policy, - tags=tags, - timeout=timeout) + return rp.add_endpoint( + name=name, + resource_group_name=resource_group_name, + endpoint_name=endpoint_name, + endpoint_resource_type=ADTEndpointType.eventhub, + endpoint_resource_name=eventhub_name, + endpoint_resource_group=eventhub_resource_group, + endpoint_resource_namespace=eventhub_namespace, + endpoint_resource_policy=eventhub_policy, + tags=tags, + timeout=timeout, + ) diff --git a/azext_iot/digitaltwins/commands_routes.py b/azext_iot/digitaltwins/commands_routes.py index 1d169209d..40c390e34 100644 --- a/azext_iot/digitaltwins/commands_routes.py +++ b/azext_iot/digitaltwins/commands_routes.py @@ -10,9 +10,13 @@ logger = get_logger(__name__) -def create_route(cmd, name, route_name, endpoint_name, filter="true", resource_group_name=None): +def create_route( + cmd, name, route_name, endpoint_name, filter="true", resource_group_name=None +): route_provider = RouteProvider(cmd=cmd, name=name, rg=resource_group_name) - return route_provider.create(route_name=route_name, endpoint_name=endpoint_name, filter=filter) + return route_provider.create( + route_name=route_name, endpoint_name=endpoint_name, filter=filter + ) def show_route(cmd, name, route_name, resource_group_name=None): diff --git a/azext_iot/digitaltwins/common.py b/azext_iot/digitaltwins/common.py index cba8af492..aeb99af92 100644 --- a/azext_iot/digitaltwins/common.py +++ b/azext_iot/digitaltwins/common.py @@ -12,24 +12,6 @@ from enum import Enum -class ADTSkuType(Enum): - """ - ADT SKU Type. - """ - - S1 = "S1" - - -class ADTLocationType(Enum): - """ - ADT Location Type. - """ - - WestCentralUS = "westcentralus" - WestUS2 = "westus2" - EastUS2EUAP = "eastus2euap" - - class ADTEndpointType(Enum): """ ADT Location Type. diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index cf2705056..ad8dc44c3 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -64,9 +64,7 @@ def load_digitaltwins_arguments(self, _): "filter", options_list=["--filter"], help="Event route filter.", ) context.argument( - "role_type", - options_list=["--role"], - help="Role name or Id.", + "role_type", options_list=["--role"], help="Role name or Id.", ) context.argument( "assignee", @@ -210,7 +208,7 @@ def load_digitaltwins_arguments(self, _): context.argument( "component_path", options_list=["--component"], - help="The path to the DTDL component." + help="The path to the DTDL component.", ) with self.argument_context("dt twin create") as context: @@ -218,7 +216,7 @@ def load_digitaltwins_arguments(self, _): "properties", options_list=["--properties", "-p"], help="Initial property values for instantiating a digital twin or related components. " - "Provide file path or inline JSON.", + "Provide file path or inline JSON.", ) with self.argument_context("dt twin telemetry") as context: @@ -236,7 +234,7 @@ def load_digitaltwins_arguments(self, _): context.argument( "component_path", options_list=["--component"], - help="The path to the DTDL component. If set telemetry will be emitted on behalf of the component." + help="The path to the DTDL component. If set, telemetry will be emitted on behalf of the component.", ) with self.argument_context("dt twin relationship create") as context: diff --git a/azext_iot/digitaltwins/providers/auth.py b/azext_iot/digitaltwins/providers/auth.py index 509ee8016..bb4cc528d 100644 --- a/azext_iot/digitaltwins/providers/auth.py +++ b/azext_iot/digitaltwins/providers/auth.py @@ -30,7 +30,9 @@ def signed_session(self, session=None): return self.refresh_session(session) - def refresh_session(self, session=None, ): + def refresh_session( + self, session=None, + ): """ Refresh requests session with SAS auth headers. @@ -47,13 +49,14 @@ def refresh_session(self, session=None, ): 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 + "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/digitaltwins/providers/generic.py b/azext_iot/digitaltwins/providers/generic.py index 84903af7a..7147e32ed 100644 --- a/azext_iot/digitaltwins/providers/generic.py +++ b/azext_iot/digitaltwins/providers/generic.py @@ -6,7 +6,13 @@ # Experimental - depends on consistency of APIs -def accumulate_result(method, token_name="continuationToken", token_arg_name="continuation_token", values_name="items", **kwargs): +def accumulate_result( + method, + token_name="continuationToken", + token_arg_name="continuation_token", + values_name="items", + **kwargs +): result_accumulator = [] nextlink = None @@ -39,5 +45,5 @@ def accumulate_result(method, token_name="continuationToken", token_arg_name="co def remove_prefix(text, prefix): if text.startswith(prefix): - return text[len(prefix):] + return text[len(prefix) :] return text diff --git a/azext_iot/digitaltwins/providers/rbac.py b/azext_iot/digitaltwins/providers/rbac.py index 3c7a9a50a..0e2710b65 100644 --- a/azext_iot/digitaltwins/providers/rbac.py +++ b/azext_iot/digitaltwins/providers/rbac.py @@ -22,8 +22,11 @@ def list_assignments(self, dt_scope, include_inherited=False, role_type=None): if role_type: filter_role_type = "--role '{}'".format(role_type) - list_op = self.cli.invoke("role assignment list --scope '{}' {} {}".format( - dt_scope, filter_role_type, include_inherited_flag)) + list_op = self.cli.invoke( + "role assignment list --scope '{}' {} {}".format( + dt_scope, filter_role_type, include_inherited_flag + ) + ) if not list_op.success(): raise CLIError("Unable to determine assignments.") diff --git a/azext_iot/digitaltwins/providers/route.py b/azext_iot/digitaltwins/providers/route.py index 35ea88e10..88968ad75 100644 --- a/azext_iot/digitaltwins/providers/route.py +++ b/azext_iot/digitaltwins/providers/route.py @@ -15,11 +15,7 @@ class RouteProvider(DigitalTwinsProvider): def __init__(self, cmd, name, rg=None): - super(RouteProvider, self).__init__( - cmd=cmd, - name=name, - rg=rg - ) + super(RouteProvider, self).__init__(cmd=cmd, name=name, rg=rg) self.sdk = self.get_sdk().event_routes def get(self, route_name): @@ -30,12 +26,11 @@ def get(self, route_name): def list(self, top=None): # top is guarded for int() in arg def from azext_iot.sdk.digitaltwins.models import EventRoutesListOptions + list_options = EventRoutesListOptions(max_item_count=top) try: - return self.sdk.list( - event_routes_list_options=list_options, - ) + return self.sdk.list(event_routes_list_options=list_options,) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) diff --git a/dev_requirements b/dev_requirements index 09395fa5d..16e42bcf1 100644 --- a/dev_requirements +++ b/dev_requirements @@ -2,12 +2,12 @@ pytest pytest-mock pytest-cov pytest-env -pylint uamqp~=1.2 mock;python_version<'3.3' -responses==0.10.12 -flake8 +responses black;python_version>='3.6' wheel==0.30.0 pre-commit six>=1.12 +pylint +flake8 From a1110b423d6f8c9a7a1ac37fffaed0fd9c3b29a5 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Fri, 5 Jun 2020 15:12:16 -0700 Subject: [PATCH 046/179] DT Twin test fix, due to service fix of component metadata (no more ). --- azext_iot/digitaltwins/_help.py | 4 +--- azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index 1a0657040..13ccb05f8 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -303,9 +303,7 @@ def load_digitaltwins_help(): az dt twin create -n {instance_name} --dtmi dtmi:example:Room;1 --twin-id {twin_id} --properties '{ "Temperature": 10.2, "Thermostat": { - "$metadata": { - "$model": "dtmi:com:example:Thermostat;1" - }, + "$metadata": {}, "setPointTemp": 23.12 } }' 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 7a86628f1..ee7768354 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -80,7 +80,7 @@ def test_dt_twin(self): { "Temperature": 10.2, "Thermostat": { - "$metadata": {"$model": self.thermostat_dtmi}, + "$metadata": {}, "setPointTemp": 23.12, }, } @@ -127,8 +127,6 @@ def test_dt_twin(self): ) ).get_output_in_json() - assert thermostat_component["$metadata"]["$model"] == self.thermostat_dtmi - self.kwargs["thermostatJsonPatch"] = json.dumps( [{"op": "replace", "path": "/setPointTemp", "value": 50.5}] ) From d6d04c07fa075ad39642a7d5d4d5d050e7d4eede Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 5 Jun 2020 15:39:11 -0700 Subject: [PATCH 047/179] Update HISTORY.rst --- HISTORY.rst | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b61c97785..46732ea1d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,37 +5,46 @@ Release History 0.9.4 +++++++++++++++ -* Azure Digital Twins Public Preview - CLI release - * Introducing 35 new commands in the following command groups - * az dt - * az dt endpoint - * az dt model - * az dt role-assignment - * az dt route - * az dt twin - * az dt twin relationship - * az dt twin telemety +Azure Digital Twins Public Preview - CLI release + +Introducing 35 new commands in the following command groups: + +* az dt +* az dt endpoint +* az dt model +* az dt role-assignment +* az dt route +* az dt twin +* az dt twin relationship +* az dt twin telemety 0.9.3 +++++++++++++++ * IoT Hub device identity import/export commands support usage via managed service identity using the --auth-type argument. + * Adds preview command group "az iot central app device" + * Adds preview command "az iot central app device create" * Adds preview command "az iot central app device show" * Adds preview command "az iot central app device list" * Adds preview command "az iot central app device delete" * Adds preview command "az iot central app device registration-info" * Adds preview command "az iot central app device registration-summary" + * Adds preview command group "az iot central app device-template" + * Adds preview command "az iot central app device-template create" * Adds preview command "az iot central app device-template show" * Adds preview command "az iot central app device-template list" * Adds preview command "az iot central app device-template delete" * Adds preview command "az iot central app device-template map" + * Changed how results are displayed in "az iot central app validate-messages" -* Known issues: +Known issues + * The following preview commands will retrieve at most 25 results + * az iot central app device list * az iot central app device-template list * az iot central app device-template map From 11179ae5b5416ec60d27146f16ecba333ecdcf1d Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 5 Jun 2020 15:39:50 -0700 Subject: [PATCH 048/179] Update HISTORY.rst --- HISTORY.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 46732ea1d..44a7b5636 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -43,11 +43,11 @@ Introducing 35 new commands in the following command groups: Known issues - * The following preview commands will retrieve at most 25 results +* The following preview commands will retrieve at most 25 results - * az iot central app device list - * az iot central app device-template list - * az iot central app device-template map + * az iot central app device list + * az iot central app device-template list + * az iot central app device-template map 0.9.2 +++++++++++++++ From dc5c45ec6e0d218e68e142b9880eebddadbb32d7 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Mon, 8 Jun 2020 15:22:19 -0700 Subject: [PATCH 049/179] Implemented telemetry:model validation logic (#190) * Implemented telemetry:model validation logic * Added unit tests, minor code refactor * Added schema_type tests * Added interface handling, TODO: validate error cases * Interface parsing is now working * add module level filtering - * Revert "add module level filtering -" This reverts commit 340a43c93e7575abaf22f493f3c548795ca02c74. * Fix UT and lint issue * Refactor code and tests to make more sense * changed abbreviated param name --- azext_iot/central/commands_device.py | 23 +- azext_iot/central/commands_device_template.py | 20 +- azext_iot/central/commands_device_twin.py | 3 +- azext_iot/central/commands_monitor.py | 5 +- azext_iot/central/models/template.py | 57 ++++ azext_iot/central/params.py | 2 +- .../central/providers/device_provider.py | 42 +-- .../providers/device_template_provider.py | 19 +- .../central/providers/monitor_provider.py | 9 +- azext_iot/central/services/device.py | 30 +- azext_iot/central/services/device_template.py | 24 +- azext_iot/constants.py | 1 + .../monitor/central_validator/__init__.py | 10 + azext_iot/monitor/central_validator/utils.py | 28 ++ .../central_validator/validate_schema.py | 51 +++ .../central_validator/validators/enum.py | 17 + .../central_validator/validators/geopoint.py | 27 ++ .../central_validator/validators/obj.py | 27 ++ .../central_validator/validators/vector.py | 22 ++ azext_iot/monitor/handlers/central_handler.py | 8 +- azext_iot/monitor/parsers/central_parser.py | 121 +++---- azext_iot/monitor/parsers/common_parser.py | 1 + azext_iot/monitor/parsers/strings.py | 9 +- .../central/json/deeply_nested_template.json | Bin 0 -> 35180 bytes azext_iot/tests/test_constants.py | 3 + .../tests/test_iot_central_validator_unit.py | 305 ++++++++++++++++++ azext_iot/tests/test_iot_utility_unit.py | 68 ---- azext_iot/tests/test_monitor_parsers_unit.py | 54 +++- 28 files changed, 718 insertions(+), 268 deletions(-) create mode 100644 azext_iot/central/models/template.py create mode 100644 azext_iot/monitor/central_validator/__init__.py create mode 100644 azext_iot/monitor/central_validator/utils.py create mode 100644 azext_iot/monitor/central_validator/validate_schema.py create mode 100644 azext_iot/monitor/central_validator/validators/enum.py create mode 100644 azext_iot/monitor/central_validator/validators/geopoint.py create mode 100644 azext_iot/monitor/central_validator/validators/obj.py create mode 100644 azext_iot/monitor/central_validator/validators/vector.py create mode 100644 azext_iot/tests/central/json/deeply_nested_template.json create mode 100644 azext_iot/tests/test_iot_central_validator_unit.py diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py index df6a36382..a45e37b47 100644 --- a/azext_iot/central/commands_device.py +++ b/azext_iot/central/commands_device.py @@ -6,22 +6,17 @@ # Dev note - think of this as a controller from knack.util import CLIError +from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central.providers import CentralDeviceProvider -def list_devices( - cmd, app_id: str, token=None, central_dns_suffix="azureiotcentral.com" -): +def list_devices(cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT): provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) return provider.list_devices() def get_device( - cmd, - app_id: str, - device_id: str, - token=None, - central_dns_suffix="azureiotcentral.com", + cmd, app_id: str, device_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) return provider.get_device(device_id) @@ -35,7 +30,7 @@ def create_device( instance_of=None, simulated=False, token=None, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ): if simulated and not instance_of: raise CLIError( @@ -52,18 +47,14 @@ def create_device( def delete_device( - cmd, - app_id: str, - device_id: str, - token=None, - central_dns_suffix="azureiotcentral.com", + cmd, app_id: str, device_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token) return provider.delete_device(device_id) def registration_info( - cmd, app_id: str, device_id, token=None, central_dns_suffix="azureiotcentral.com", + cmd, app_id: str, device_id, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token,) @@ -73,7 +64,7 @@ def registration_info( def registration_summary( - cmd, app_id: str, token=None, central_dns_suffix="azureiotcentral.com", + cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): provider = CentralDeviceProvider(cmd=cmd, app_id=app_id, token=token,) return provider.get_device_registration_summary( diff --git a/azext_iot/central/commands_device_template.py b/azext_iot/central/commands_device_template.py index 8a497a45d..d75145e1a 100644 --- a/azext_iot/central/commands_device_template.py +++ b/azext_iot/central/commands_device_template.py @@ -7,6 +7,7 @@ from knack.util import CLIError +from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.common import utility from azext_iot.central.providers import CentralDeviceTemplateProvider @@ -16,23 +17,25 @@ def get_device_template( app_id: str, device_template_id: str, token=None, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ): provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) - return provider.get_device_template( + template = provider.get_device_template( device_template_id=device_template_id, central_dns_suffix=central_dns_suffix ) + return template.template def list_device_templates( - cmd, app_id: str, token=None, central_dns_suffix="azureiotcentral.com" + cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT ): provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) - return provider.list_device_templates(central_dns_suffix=central_dns_suffix) + templates = provider.list_device_templates(central_dns_suffix=central_dns_suffix) + return {template.id: template.template for template in templates.values()} def map_device_templates( - cmd, app_id: str, token=None, central_dns_suffix="azureiotcentral.com" + cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT ): provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) return provider.map_device_templates(central_dns_suffix=central_dns_suffix) @@ -44,7 +47,7 @@ def create_device_template( device_template_id: str, content: str, token=None, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ): if not isinstance(content, str): raise CLIError("content must be a string: {}".format(content)) @@ -52,11 +55,12 @@ def create_device_template( payload = utility.process_json_arg(content, argument_name="content") provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) - return provider.create_device_template( + template = provider.create_device_template( device_template_id=device_template_id, payload=payload, central_dns_suffix=central_dns_suffix, ) + return template.template def delete_device_template( @@ -64,7 +68,7 @@ def delete_device_template( app_id: str, device_template_id: str, token=None, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ): provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) return provider.delete_device_template( diff --git a/azext_iot/central/commands_device_twin.py b/azext_iot/central/commands_device_twin.py index b61a575c4..03d766d1d 100644 --- a/azext_iot/central/commands_device_twin.py +++ b/azext_iot/central/commands_device_twin.py @@ -5,6 +5,7 @@ # -------------------------------------------------------------------------------------------- from knack.util import CLIError +from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot._factory import _bind_sdk from azext_iot.common.shared import SdkType from azext_iot.common.utility import unpack_msrest_error @@ -15,7 +16,7 @@ def find_between(s, start, end): return (s.split(start))[1].split(end)[0] -def device_twin_show(cmd, device_id, app_id, central_dns_suffix="azureiotcentral.com"): +def device_twin_show(cmd, device_id, app_id, central_dns_suffix=CENTRAL_ENDPOINT): from azext_iot.common._azure import get_iot_central_tokens tokens = get_iot_central_tokens(cmd, app_id, central_dns_suffix) diff --git a/azext_iot/central/commands_monitor.py b/azext_iot/central/commands_monitor.py index 60cfe4b66..c3953e135 100644 --- a/azext_iot/central/commands_monitor.py +++ b/azext_iot/central/commands_monitor.py @@ -5,6 +5,7 @@ # -------------------------------------------------------------------------------------------- from azure.cli.core.commands import AzCliCommand +from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central.providers.monitor_provider import MonitorProvider from azext_iot.monitor.models.enum import Severity from azext_iot.monitor.models.arguments import ( @@ -29,7 +30,7 @@ def validate_messages( duration=300, style="scroll", minimum_severity=Severity.warning.name, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ): telemetry_args = TelemetryArguments( cmd, @@ -74,7 +75,7 @@ def monitor_events( repair=False, properties=None, yes=False, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ): telemetry_args = TelemetryArguments( cmd, diff --git a/azext_iot/central/models/template.py b/azext_iot/central/models/template.py new file mode 100644 index 000000000..6fdfcb992 --- /dev/null +++ b/azext_iot/central/models/template.py @@ -0,0 +1,57 @@ +# 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 Template: + def __init__(self, template: dict): + self.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) + except: + raise CLIError("Could not parse iot central device template.") + + def get_schema(self, telemetry_name, interface_name=""): + # interface_name has been specified, do a pointed lookup + if interface_name: + interface = self.interfaces.get(interface_name, {}) + return interface.get(telemetry_name) + + # find first matching telemetry_name in any interface + for interface in self.interfaces.values(): + schema = interface.get(telemetry_name) + if schema: + return schema + + # not found + return None + + def _extract_interfaces(self, template: dict) -> dict: + try: + dcm = template.get("capabilityModel", {}) + interfaces = dcm.get("implements", {}) + return { + interface["name"]: 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, interface: dict) -> dict: + return {schema["name"]: schema for schema in interface["schema"]["contents"]} + + def _extract_schema_names(self, interfaces: dict) -> dict: + return { + interface_name: list(interface_schemas.keys()) + for interface_name, interface_schemas in interfaces.items() + } diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 647424354..6cef69288 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -53,7 +53,7 @@ def load_central_arguments(self, _): ) context.argument( "device_template_id", - options_list=["--device-template-id"], + options_list=["--device-template-id", "--dtid"], help="Device template id. Example: somedevicetemplate", ) context.argument( diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index d4ce0dde1..53ed84195 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -7,6 +7,7 @@ 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 @@ -36,9 +37,7 @@ 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="azureiotcentral.com", - ) -> Device: + def get_device(self, device_id, central_dns_suffix=CENTRAL_ENDPOINT) -> Device: if not device_id: raise CLIError("Device id must be specified.") # get or add to cache @@ -58,30 +57,7 @@ def get_device( return device - def get_device_template_by_device_id( - self, device_id, central_dns_suffix="azureiotcentral.com", - ) -> dict: - from azext_iot.central.providers import CentralDeviceTemplateProvider - - if not device_id: - raise CLIError("Device id must be specified.") - - device = self.get_device(device_id, central_dns_suffix) - if not device.instance_of: - raise CLIError( - "Device '{}' does not have a corresponding device template.".format( - device_id - ) - ) - - template = CentralDeviceTemplateProvider.get_device_template( - self=self, - device_template_id=device.instance_of, - central_dns_suffix=central_dns_suffix, - ) - return template - - def list_devices(self, central_dns_suffix="azureiotcentral.com") -> List[Device]: + def list_devices(self, central_dns_suffix=CENTRAL_ENDPOINT) -> List[Device]: devices = central_services.device.list_devices( cmd=self._cmd, app_id=self._app_id, @@ -100,7 +76,7 @@ def create_device( device_name=None, instance_of=None, simulated=False, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ) -> Device: if not device_id: raise CLIError("Device id must be specified.") @@ -127,9 +103,7 @@ def create_device( return device - def delete_device( - self, device_id, central_dns_suffix="azureiotcentral.com", - ) -> dict: + def delete_device(self, device_id, central_dns_suffix=CENTRAL_ENDPOINT,) -> dict: if not device_id: raise CLIError("Device id must be specified.") @@ -150,7 +124,7 @@ def delete_device( return result def get_device_credentials( - self, device_id, central_dns_suffix="azureiotcentral.com", + self, device_id, central_dns_suffix=CENTRAL_ENDPOINT, ) -> dict: credentials = self._device_credentials.get(device_id) @@ -177,7 +151,7 @@ def get_device_registration_info( self, device_id, device_status: DeviceStatus, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ) -> dict: dps_state = {} info = self._device_registration_info.get(device_id) @@ -207,7 +181,7 @@ def get_device_registration_info( return info - def get_device_registration_summary(self, central_dns_suffix="azureiotcentral.com"): + def get_device_registration_summary(self, central_dns_suffix=CENTRAL_ENDPOINT): return central_services.device.get_device_registration_summary( cmd=self._cmd, app_id=self._app_id, diff --git a/azext_iot/central/providers/device_template_provider.py b/azext_iot/central/providers/device_template_provider.py index e7bc3ff0a..2a8f72597 100644 --- a/azext_iot/central/providers/device_template_provider.py +++ b/azext_iot/central/providers/device_template_provider.py @@ -5,6 +5,7 @@ # -------------------------------------------------------------------------------------------- from knack.util import CLIError +from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central import services as central_services @@ -27,7 +28,7 @@ def __init__(self, cmd, app_id, token=None): self._device_templates = {} def get_device_template( - self, device_template_id, central_dns_suffix="azureiotcentral.com", + self, device_template_id, central_dns_suffix=CENTRAL_ENDPOINT, ): # get or add to cache device_template = self._device_templates.get(device_template_id) @@ -51,20 +52,18 @@ def get_device_template( return device_template def list_device_templates( - self, central_dns_suffix="azureiotcentral.com", + self, central_dns_suffix=CENTRAL_ENDPOINT, ): templates = central_services.device_template.list_device_templates( cmd=self._cmd, app_id=self._app_id, token=self._token ) - self._device_templates.update( - {template["id"]: template for template in templates} - ) + self._device_templates.update({template.id: template for template in templates}) return self._device_templates def map_device_templates( - self, central_dns_suffix="azureiotcentral.com", + self, central_dns_suffix=CENTRAL_ENDPOINT, ): """ Maps each template name to the corresponding template id @@ -72,13 +71,13 @@ def map_device_templates( templates = central_services.device_template.list_device_templates( cmd=self._cmd, app_id=self._app_id, token=self._token ) - return {template["displayName"]: template["id"] for template in templates} + return {template.name: template.id for template in templates} def create_device_template( self, device_template_id: str, payload: str, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ): template = central_services.device_template.create_device_template( cmd=self._cmd, @@ -89,12 +88,12 @@ def create_device_template( central_dns_suffix=central_dns_suffix, ) - self._device_templates[template["id"]] = template + self._device_templates[template.id] = template return template def delete_device_template( - self, device_template_id, central_dns_suffix="azureiotcentral.com", + self, device_template_id, central_dns_suffix=CENTRAL_ENDPOINT, ): if not device_template_id: raise CLIError("Device template id must be specified.") diff --git a/azext_iot/central/providers/monitor_provider.py b/azext_iot/central/providers/monitor_provider.py index 8ce9664c6..eb3bfd62a 100644 --- a/azext_iot/central/providers/monitor_provider.py +++ b/azext_iot/central/providers/monitor_provider.py @@ -6,7 +6,10 @@ from azure.cli.core.commands import AzCliCommand -from azext_iot.central.providers import CentralDeviceProvider +from azext_iot.central.providers import ( + CentralDeviceProvider, + CentralDeviceTemplateProvider, +) from azext_iot.monitor.models.arguments import ( CentralHandlerArguments, @@ -28,6 +31,7 @@ def __init__( central_dns_suffix: str, ): central_device_provider = CentralDeviceProvider(cmd, app_id) + central_template_provider = CentralDeviceTemplateProvider(cmd, app_id) self._targets = self._build_targets( cmd=cmd, app_id=app_id, @@ -36,6 +40,7 @@ def __init__( ) self._handler = self._build_handler( central_device_provider=central_device_provider, + central_template_provider=central_template_provider, central_handler_args=central_handler_args, ) @@ -80,11 +85,13 @@ def _build_targets( def _build_handler( self, central_device_provider: CentralDeviceProvider, + central_template_provider: CentralDeviceTemplateProvider, central_handler_args: CentralHandlerArguments, ): from azext_iot.monitor.handlers import CentralHandler return CentralHandler( central_device_provider=central_device_provider, + central_template_provider=central_template_provider, central_handler_args=central_handler_args, ) diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index 0ea42d480..1b5368c1b 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -8,11 +8,13 @@ import requests from knack.util import CLIError -from typing import List -from azext_iot.central.models.enum import DeviceStatus 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 logger = get_logger(__name__) @@ -20,11 +22,7 @@ def get_device( - cmd, - app_id: str, - device_id: str, - token: str, - central_dns_suffix="azureiotcentral.com", + cmd, app_id: str, device_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, ) -> Device: """ Get device info given a device id @@ -50,7 +48,7 @@ def get_device( def list_devices( - cmd, app_id: str, token: str, max_pages=1, central_dns_suffix="azureiotcentral.com", + cmd, app_id: str, token: str, max_pages=1, central_dns_suffix=CENTRAL_ENDPOINT, ) -> List[Device]: """ Get a list of all devices in IoTC app @@ -88,7 +86,7 @@ def list_devices( def get_device_registration_summary( - cmd, app_id: str, token: str, central_dns_suffix="azureiotcentral.com", + cmd, app_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, ): """ Get device registration summary for a given app @@ -134,7 +132,7 @@ def create_device( instance_of: str, simulated: bool, token: str, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ) -> Device: """ Create a device in IoTC @@ -174,11 +172,7 @@ def create_device( def delete_device( - cmd, - app_id: str, - device_id: str, - token: str, - central_dns_suffix="azureiotcentral.com", + cmd, app_id: str, device_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, ) -> dict: """ Delete a device from IoTC @@ -203,11 +197,7 @@ def delete_device( def get_device_credentials( - cmd, - app_id: str, - device_id: str, - token: str, - central_dns_suffix="azureiotcentral.com", + cmd, app_id: str, device_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, ): """ Get device credentials from IoTC diff --git a/azext_iot/central/services/device_template.py b/azext_iot/central/services/device_template.py index 46d39889b..44d3a9590 100644 --- a/azext_iot/central/services/device_template.py +++ b/azext_iot/central/services/device_template.py @@ -7,9 +7,13 @@ 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 logger = get_logger(__name__) @@ -21,8 +25,8 @@ def get_device_template( app_id: str, device_template_id: str, token: str, - central_dns_suffix="azureiotcentral.com", -) -> dict: + central_dns_suffix=CENTRAL_ENDPOINT, +) -> Template: """ Get a specific device template from IoTC @@ -43,12 +47,12 @@ def get_device_template( headers = _utility.get_headers(token, cmd) response = requests.get(url, headers=headers) - return _utility.try_extract_result(response) + return Template(_utility.try_extract_result(response)) def list_device_templates( - cmd, app_id: str, token: str, central_dns_suffix="azureiotcentral.com", -) -> list: + cmd, app_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, +) -> List[Template]: """ Get a list of all device templates in IoTC @@ -73,7 +77,7 @@ def list_device_templates( if "value" not in result: raise CLIError("Value is not present in body: {}".format(result)) - return result["value"] + return [Template(item) for item in result["value"]] def create_device_template( @@ -82,8 +86,8 @@ def create_device_template( device_template_id: str, payload: dict, token: str, - central_dns_suffix="azureiotcentral.com", -) -> list: + central_dns_suffix=CENTRAL_ENDPOINT, +) -> Template: """ Create a device template in IoTC @@ -109,7 +113,7 @@ def create_device_template( headers = _utility.get_headers(token, cmd, has_json_payload=True) response = requests.put(url, headers=headers, json=payload) - return _utility.try_extract_result(response) + return Template(_utility.try_extract_result(response)) def delete_device_template( @@ -117,7 +121,7 @@ def delete_device_template( app_id: str, device_template_id: str, token: str, - central_dns_suffix="azureiotcentral.com", + central_dns_suffix=CENTRAL_ENDPOINT, ) -> dict: """ Delete a device template from IoTC diff --git a/azext_iot/constants.py b/azext_iot/constants.py index c549b6bce..4815234c6 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -35,6 +35,7 @@ MIN_SIM_MSG_COUNT = 1 SIM_RECEIVE_SLEEP_SEC = 3 PNP_API_VERSION = "2019-07-01-preview" +CENTRAL_ENDPOINT = "azureiotcentral.com" PNP_ENDPOINT = "https://provider.azureiotrepository.com" PNP_REPO_ENDPOINT = "https://repo.azureiotrepository.com" DEVICE_DEVICESCOPE_PREFIX = "ms-azure-iot-edge://" diff --git a/azext_iot/monitor/central_validator/__init__.py b/azext_iot/monitor/central_validator/__init__.py new file mode 100644 index 000000000..ec5fc16b0 --- /dev/null +++ b/azext_iot/monitor/central_validator/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.monitor.central_validator.validate_schema import validate +from azext_iot.monitor.central_validator.utils import extract_schema_type + +__all__ = ["validate", "extract_schema_type"] diff --git a/azext_iot/monitor/central_validator/utils.py b/azext_iot/monitor/central_validator/utils.py new file mode 100644 index 000000000..c00780ebd --- /dev/null +++ b/azext_iot/monitor/central_validator/utils.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def extract_schema_type(schema: dict): + # some error with parsing schema + if not isinstance(schema, dict): + return + + schema_type = schema.get("schema") + # some error with parsing schema + if not schema_type: + return + + # Custom defined complex types store schema as dict + if not isinstance(schema_type, str): + schema_type = schema_type["@type"] + + # If template is retrieved through API, the type info is in a list + # Extract the first item + # TODO: update this work around once IoTC has consistency between API and UX + if isinstance(schema_type, list): + schema_type = schema_type[0] + + return schema_type diff --git a/azext_iot/monitor/central_validator/validate_schema.py b/azext_iot/monitor/central_validator/validate_schema.py new file mode 100644 index 000000000..cc97da387 --- /dev/null +++ b/azext_iot/monitor/central_validator/validate_schema.py @@ -0,0 +1,51 @@ +# 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.common.utility import ISO8601Validator +from azext_iot.monitor.central_validator import utils +from azext_iot.monitor.central_validator.validators import enum, geopoint, obj, vector + +iso_validator = ISO8601Validator() + +validation_function_factory = { + # primitive + "boolean": lambda schema, value: isinstance(value, bool), + "double": lambda schema, value: isinstance(value, (float, int)), + "float": lambda schema, value: isinstance(value, (float, int)), + "integer": lambda schema, value: isinstance(value, int), + "long": lambda schema, value: isinstance(value, (float, int)), + "string": lambda schema, value: isinstance(value, str), + # primitive - time + "date": lambda schema, value: iso_validator.is_iso8601_date(value), + "dateTime": lambda schema, value: iso_validator.is_iso8601_datetime(value), + "duration": lambda schema, value: iso_validator.is_iso8601_duration(value), + "time": lambda schema, value: iso_validator.is_iso8601_time(value), + # pre-defined complex + "geopoint": geopoint.validate, + "vector": vector.validate, + # complex + "Enum": enum.validate, + "Object": obj.validate, + # TODO: yet to be enabled in prod "Map": lambda val: False, + # TODO: yet to be implemented in prod "Array": lambda val: False, +} + + +def validate(schema, value): + # if theres nothing to validate, then its valid + if value is None: + return True + + schema_type = utils.extract_schema_type(schema) + if not schema_type: + return False + + validate_function = validation_function_factory.get(schema_type) + + # invalid schema type detected + if not validate_function: + return False + + return validate_function(schema, value) diff --git a/azext_iot/monitor/central_validator/validators/enum.py b/azext_iot/monitor/central_validator/validators/enum.py new file mode 100644 index 000000000..8dd0179c3 --- /dev/null +++ b/azext_iot/monitor/central_validator/validators/enum.py @@ -0,0 +1,17 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def validate(schema, value): + if not isinstance(schema, dict): + return False + + # schema.schema.enumValues, but done safely + enum_values = schema.get("schema", {}).get("enumValues", []) + + allowed_values = [item["enumValue"] for item in enum_values if "enumValue" in item] + + return value in allowed_values diff --git a/azext_iot/monitor/central_validator/validators/geopoint.py b/azext_iot/monitor/central_validator/validators/geopoint.py new file mode 100644 index 000000000..4b4661c69 --- /dev/null +++ b/azext_iot/monitor/central_validator/validators/geopoint.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def validate(schema, value: dict): + required_keys = set(["lat", "lon"]) + all_keys = set(["alt", "lat", "lon"]) + if not isinstance(value, dict): + return False + + # check that value has all required keys + if not set(required_keys).issubset(set(value.keys())): + return False + + # check that value does not have more than expected keys + if not set(value.keys()).issubset(all_keys): + return False + + # check that all values are double + for val in value.values(): + if not isinstance(val, (float, int)): + return False + + return True diff --git a/azext_iot/monitor/central_validator/validators/obj.py b/azext_iot/monitor/central_validator/validators/obj.py new file mode 100644 index 000000000..47e1620eb --- /dev/null +++ b/azext_iot/monitor/central_validator/validators/obj.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.monitor.central_validator import validate_schema + + +def validate(schema: dict, value: dict): + if not isinstance(schema, dict): + return False + + if not isinstance(value, dict): + return False + + fields = schema.get("schema", {}).get("fields", []) + schema_fields = {field["name"]: field for field in fields} + + for key, val in value.items(): + if key not in schema_fields: + return False + + if not validate_schema.validate(schema_fields[key], val): + return False + + return True diff --git a/azext_iot/monitor/central_validator/validators/vector.py b/azext_iot/monitor/central_validator/validators/vector.py new file mode 100644 index 000000000..e19b4baec --- /dev/null +++ b/azext_iot/monitor/central_validator/validators/vector.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. +# -------------------------------------------------------------------------------------------- + + +def validate(schema, value: dict): + required_keys = set(["x", "y", "z"]) + if not isinstance(value, dict): + return False + + # check that value has all required keys + if set(value.keys()) != required_keys: + return False + + # check that all values are double + for val in value.values(): + if not isinstance(val, (float, int)): + return False + + return True diff --git a/azext_iot/monitor/handlers/central_handler.py b/azext_iot/monitor/handlers/central_handler.py index a46bc558d..a491bb55c 100644 --- a/azext_iot/monitor/handlers/central_handler.py +++ b/azext_iot/monitor/handlers/central_handler.py @@ -7,7 +7,10 @@ from typing import List from azext_iot.monitor.utility import stop_monitor, get_loop -from azext_iot.central.providers.device_provider import CentralDeviceProvider +from azext_iot.central.providers import ( + CentralDeviceProvider, + CentralDeviceTemplateProvider, +) from azext_iot.monitor.handlers.common_handler import CommonHandler from azext_iot.monitor.models.arguments import CentralHandlerArguments from azext_iot.monitor.parsers.central_parser import CentralParser @@ -18,6 +21,7 @@ class CentralHandler(CommonHandler): def __init__( self, central_device_provider: CentralDeviceProvider, + central_template_provider: CentralDeviceTemplateProvider, central_handler_args: CentralHandlerArguments, ): super(CentralHandler, self).__init__( @@ -25,6 +29,7 @@ def __init__( ) self._central_device_provider = central_device_provider + self._central_template_provider = central_template_provider self._central_handler_args = central_handler_args @@ -43,6 +48,7 @@ def validate_message(self, message): message=message, common_parser_args=self._common_handler_args.common_parser_args, central_device_provider=self._central_device_provider, + central_template_provider=self._central_template_provider, ) if not self._should_process_device(parser.device_id): diff --git a/azext_iot/monitor/parsers/central_parser.py b/azext_iot/monitor/parsers/central_parser.py index 6c293130d..11e2cad8a 100644 --- a/azext_iot/monitor/parsers/central_parser.py +++ b/azext_iot/monitor/parsers/central_parser.py @@ -8,15 +8,17 @@ from uamqp.message import Message -from azext_iot.common.utility import ISO8601Validator -from azext_iot.central.providers import CentralDeviceProvider +from azext_iot.central.providers import ( + CentralDeviceProvider, + CentralDeviceTemplateProvider, +) +from azext_iot.central.models.template import Template 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 from azext_iot.monitor.models.enum import Severity from azext_iot.monitor.parsers.common_parser import CommonParser -ios_validator = ISO8601Validator() - class CentralParser(CommonParser): def __init__( @@ -24,11 +26,13 @@ def __init__( message: Message, common_parser_args: CommonParserArguments, central_device_provider: CentralDeviceProvider, + central_template_provider: CentralDeviceTemplateProvider, ): super(CentralParser, self).__init__( message=message, common_parser_args=common_parser_args ) self._central_device_provider = central_device_provider + self._central_template_provider = central_template_provider self._template_id = None def _add_central_issue(self, severity: Severity, details: str): @@ -48,7 +52,7 @@ def parse_message(self) -> dict: self._perform_static_validations(payload=payload) # disable dynamic validations until Microservices work is figured out - # self._perform_dynamic_validations(payload=payload) + self._perform_dynamic_validations(payload=payload) return parsed_message @@ -83,101 +87,62 @@ def _perform_dynamic_validations(self, payload: dict): if not isinstance(payload, dict): return - template = self._get_device_template() + template = self._get_template() - # _get_device_template should log error if there was an issue - if not template: + if not isinstance(template, Template): return - template_schemas = self._extract_template_schemas_from_template( - template=template - ) - - # _extract_template_schemas_from_template should log error if there was an issue - if not isinstance(template_schemas, dict): + # pnp device is sending data to an unrecognized interface + if self.interface_name and (self.interface_name not in template.interfaces): + details = strings.invalid_interface_name( + self.interface_name, list(template.interfaces.keys()) + ) + self._add_central_issue(severity=Severity.warning, details=details) return - self._validate_payload_against_schema( - payload=payload, template_schemas=template_schemas, + self._validate_payload_against_interfaces( + payload=payload, template=template, ) - def _get_device_template(self): + def _get_template(self): try: - return self._central_device_provider.get_device_template_by_device_id( - self.device_id + device = self._central_device_provider.get_device(self.device_id) + template = self._central_template_provider.get_device_template( + device.instance_of ) + self._template_id = template.id + return template except Exception as e: details = strings.device_template_not_found(e) self._add_central_issue(severity=Severity.error, details=details) - def _extract_template_schemas_from_template(self, template: dict): - try: - self._template_id = template.get("id") - schemas = [] - dcm = template["capabilityModel"] - implements = dcm["implements"] - for implementation in implements: - contents = implementation["schema"]["contents"] - schemas.extend(contents) - return {schema["name"]: schema for schema in schemas} - except Exception: - details = strings.invalid_template_extract_schema_failed(template) - self._add_central_issue(severity=Severity.error, details=details) - # 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_against_schema( - self, payload: dict, template_schemas: dict, + def _validate_payload_against_interfaces( + self, payload: dict, template: Template, ): - template_schema_names = template_schemas.keys() name_miss = [] - for name, value in payload.items(): - schema = template_schemas.get(name) + for telemetry_name, telemetry in payload.items(): + schema = template.get_schema( + telemetry_name=telemetry_name, interface_name=self.interface_name + ) if not schema: - name_miss.append(name) - - is_dict = isinstance(schema, dict) - if is_dict and not self._validate_types_match(value, schema): - expected_type = str(schema.get("schema")) - details = strings.invalid_primitive_schema_mismatch_template( - name, expected_type, value - ) - self._add_central_issue(severity=Severity.error, details=details) + name_miss.append(telemetry_name) + else: + self._process_telemetry(telemetry_name, schema, telemetry) if name_miss: details = strings.invalid_field_name_mismatch_template( - name_miss, list(template_schema_names) + name_miss, template.schema_names ) self._add_central_issue(severity=Severity.warning, details=details) - def _validate_types_match(self, value, schema: dict) -> bool: - # suppress error if there is no "schema" in schema - # means something else went wrong - schema_type = schema.get("schema") - if not schema_type: - return True - - if schema_type == "boolean": - return isinstance(value, bool) - elif schema_type == "double": - return isinstance(value, (float, int)) - elif schema_type == "float": - return isinstance(value, (float, int)) - elif schema_type == "integer": - return isinstance(value, int) - elif schema_type == "long": - return isinstance(value, (float, int)) - elif schema_type == "string": - return isinstance(value, str) - elif schema_type == "time": - return ios_validator.is_iso8601_time(value) - elif schema_type == "date": - return ios_validator.is_iso8601_date(value) - elif schema_type == "dateTime": - return ios_validator.is_iso8601_datetime(value) - elif schema_type == "duration": - return ios_validator.is_iso8601_duration(value) - - # if the schema_type is not found above, suppress error - return True + def _process_telemetry(self, telemetry_name: str, schema, telemetry): + expected_type = extract_schema_type(schema) + is_payload_valid = validate(schema, telemetry) + if expected_type and not is_payload_valid: + details = strings.invalid_primitive_schema_mismatch_template( + telemetry_name, expected_type, telemetry + ) + self._add_central_issue(severity=Severity.error, details=details) diff --git a/azext_iot/monitor/parsers/common_parser.py b/azext_iot/monitor/parsers/common_parser.py index 527969a59..bc86d0eb1 100644 --- a/azext_iot/monitor/parsers/common_parser.py +++ b/azext_iot/monitor/parsers/common_parser.py @@ -25,6 +25,7 @@ def __init__(self, message: Message, common_parser_args: CommonParserArguments): self.issues_handler = IssueHandler() self._common_parser_args = common_parser_args self._message = message + self.device_id = "" # need to default self.device_id = self._parse_device_id(message) self.interface_name = self._parse_interface_name(message) diff --git a/azext_iot/monitor/parsers/strings.py b/azext_iot/monitor/parsers/strings.py index a241f2566..54ac4379c 100644 --- a/azext_iot/monitor/parsers/strings.py +++ b/azext_iot/monitor/parsers/strings.py @@ -55,6 +55,13 @@ def invalid_custom_headers(): ).format(SUPPORTED_MESSAGE_HEADERS) +# warning +def invalid_interface_name(interface_name: str, allowed_interfaces: list): + return ( + "Device is specifying an interface that is unknown. Device specified interface: '{}'. Allowed interfaces: '{}'." + ).format(interface_name, allowed_interfaces) + + # warning def invalid_field_name_mismatch_template( unmodeled_capabilities: list, modeled_capabilities: list @@ -62,7 +69,7 @@ def invalid_field_name_mismatch_template( return ( "Device is sending data that has not been defined in the device template. " "Following capabilities have NOT been defined in the device template '{}'. " - "Following capabilities have been defined in the device template '{}'. " + "Following capabilities have been defined in the device template (grouped by interface) '{}'. " ).format(unmodeled_capabilities, modeled_capabilities) diff --git a/azext_iot/tests/central/json/deeply_nested_template.json b/azext_iot/tests/central/json/deeply_nested_template.json new file mode 100644 index 0000000000000000000000000000000000000000..de66b86a4266646f0e390922d025050aeb0b8a5b GIT binary patch literal 35180 zcmeHQS#KLR5ax4${)b{;)1>j0Gy)2wahf)0(<4b!plx8tmT%dzCCjm0xIew^H%pH> z-1{K8O0p2-U9HKP;c(`gA-UZB@87@GXX=jn9G^&Ssi8W-cT?@FF}`Q&QoU3YHB#I7 z9jO6Inew{M6V#fhW0c)i7x=47{dr53O6ih&`0p9$57d|V4AqsIt0`*jqyCw?AJaL) zf6mns{@PS$xbgz!-UxZ_s{=KQc}Etvcj^o4cch+x$LL0~DCT+$DX8ud_*0unDr)66 z&c0-gTF`d^LQXY6sffLd)q2_5Kh?`fb}6*G%>eU#DIu>Z3HjWKS4M#|8acY@?6V zA@r0_d4`tlp>0RtkmcDd10CIakd`jXhmw4&_M?_FOSjUWB=x-h)07PU5Vqy}fuxx2 z6S9OqO}>gTZXdlm2ZBxTccy*@k{_Utx6pzzU9*%VVI7^DdW4hX*rK*UVG10~_8YE2 z$D%&qa%KKh*77_bVES~7;ORD!x}jRf%WQdE<0yMvS-S@6RIR~wHq>Jv$OfYEc4gbq znrvo2x~{a?a_gZwRV+x;E=FaWvmMu|e%2>K#MqCmepC@5B$aZskfpmsjx3A#5&m+C zwolbJwd{l2)D&m9J{=?VT12tuw|!B~a+p$83*z!+cqBDb1$&sTZc!44+EHtYKekMZ zg+)>$1B@bO$PLFpsyVX43q-!1M)s(+-yFMajoE0Dmx0JS^1QA^YT4c6o0O>?)=5hB za8K4;Yz)k{#Am?B6XZjC7%w(M%A^LGWM`6wJa>bjl429v<@#mFYU{EtwlWtwrt>|Q zv|XUCE$xSgz_vaIz~`a*3P??}Ky&p7`SwO*oKEV-rAKJ%XoufYzBc}#W$}yaQdV~u zFR3=idr9SE!2WC>FrKpP#3NX1E;4MUl{~tS88s4AdhE9*qe15Pvr?kGF&sN!Kwrse9MsMM+(Vej+-SXdF~4%m`52_$ewjI z&Su`pV^Q9)>THysY`f zqxsqqyY8A~T^sd{Il4{V%q!&S!vB0j9%sL~i{#p4jSH+X5pgXT@^CM9g+PXXsCg0Q zm^_BSCgxc^H(K+aov=)FEXtEYOVUNoWb5K%U0*jiLu-rK>m8;{{(OunLu0e-{K=Fd zOk2i!{~_)nJ;8U5FWmv-b6!XGr#$Z(?X<6j&Anv%Lt>q7>tf|(QWxFjXHtrAxtiqK z=WXuuwrq?uIQURKi+5sd#`_|=KtHyCML=fDD9e~o$j0J{UuyyvILnm*3MVjj1k$G)zy&)D8lCrBtQA=Lee%roy8-DcB9J7F=BB@2y} z%(pDhG_K4#&kM$e@{afF^5Q{`C=X1-kS zkgu{!(*H_k+1PA=oFyxVHkR&oH>b3%D(79#M@Sj_I8r^1xZ5Ktd!WjBejHg|z&JvB z3Li&8atxjGM0nO0kH6#!9Y0%gABaa!#N5Sq8c_TFT0<4@O_%A%l09xC?@nfI2B%WQ;~cCkq4-kn@o$bK%oCydqjA0ha?v&x_R zeki-whFdw^QFGJM_VTo&(7YAR_|~sgPr~Z->*Ra&a<;|IyPh`6SzL@P-HZ=UUp}R5ZPRy-F_Q)-Du@>I;IXrbO zQp;X^w~f6cte#bB`mUahQ{MuKzm{Q9_f2XH%(6te+Sm*!x%I387Z%Di-5@Aaqhk}> z<@#mFYU{EtwlWtwrg7FKZ5OC(OZ(y5J-stwj`f~%%*4@~uEvh;nRk&#NL_2|u+g&3 zOhg-h(6ac&b=h`5`&FCcy)E0v>I3{VePejl8)Q9h>p3U#p&k)#+c?7z@_J%*7p{tv4Er zxj-y8$W#}B%8srj6lyM({AKfKAMjml5UKgIl`$$IkMVY;RcF!BxEDcBYtJ8oHW0a-v()INlT6(!5DStkl+`{Pj`jW4kOZqd=f;up3yU{ xUzwek=IIgXIurB&QJ;c3^_eWoQrgzdEjJ~(oYoEXIrS>(QT7{`(q40|{s+=)x`_Y) literal 0 HcmV?d00001 diff --git a/azext_iot/tests/test_constants.py b/azext_iot/tests/test_constants.py index 9519aa82e..242823530 100644 --- a/azext_iot/tests/test_constants.py +++ b/azext_iot/tests/test_constants.py @@ -7,4 +7,7 @@ class FileNames: central_device_template_file = "central/json/device_template.json" + central_deeply_nested_device_template_file = ( + "central/json/deeply_nested_template.json" + ) central_device_file = "central/json/device.json" diff --git a/azext_iot/tests/test_iot_central_validator_unit.py b/azext_iot/tests/test_iot_central_validator_unit.py new file mode 100644 index 000000000..c401e0a40 --- /dev/null +++ b/azext_iot/tests/test_iot_central_validator_unit.py @@ -0,0 +1,305 @@ +# 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.central.models.template import Template +from azext_iot.monitor.central_validator import validate, extract_schema_type + +from .helpers import load_json +from .test_constants import FileNames + + +class TestExtractSchemaType: + def test_extract_schema_type(self): + expected_mapping = { + "Bool": "boolean", + "Date": "date", + "DateTime": "dateTime", + "Double": "double", + "Duration": "duration", + "IntEnum": "Enum", + "StringEnum": "Enum", + "Float": "float", + "Geopoint": "geopoint", + "Long": "long", + "Object": "Object", + "String": "string", + "Time": "time", + "Vector": "vector", + } + template = Template(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) + assert schema_type == val + + +class TestPrimitiveValidations: + @pytest.mark.parametrize( + "value, expected_result", + [ + (True, True), + (False, True), + ("False", False), + ("True", False), + (1, False), + (0, False), + ], + ) + def test_boolean(self, value, expected_result): + assert validate({"schema": "boolean"}, value) == expected_result + + @pytest.mark.parametrize( + "value, expected_result", + [(1, True), (-1, True), (1.1, True), ("1", False), ("1.1", False)], + ) + def test_double_float_long(self, value, expected_result): + assert validate({"schema": "double"}, value) == expected_result + assert validate({"schema": "float"}, value) == expected_result + assert validate({"schema": "long"}, value) == expected_result + + @pytest.mark.parametrize( + "value, expected_result", + [(1, True), (-1, True), (1.1, False), ("1", False), ("1.1", False)], + ) + def test_int(self, value, expected_result): + assert validate({"schema": "integer"}, value) == expected_result + + @pytest.mark.parametrize( + "value, expected_result", + [("a", True), ("asd", True), (1, False), (True, False)], + ) + def test_str(self, value, expected_result): + assert validate({"schema": "string"}, value) == expected_result + + # by convention we have stated that an empty payload is valid + def test_empty(self): + assert validate(None, None) + + +# none of these are valid anything in ISO 8601 +BAD_ARRAY = ["asd", "", 123.4, 123, True, False] + + +class TestDateTimeValidations: + # Success suite + @pytest.mark.parametrize( + "to_validate", ["20200101", "20200101Z", "2020-01-01", "2020-01-01Z"] + ) + def test_is_iso8601_date_pass(self, to_validate): + assert validate({"schema": "date"}, to_validate) + + @pytest.mark.parametrize( + "to_validate", + [ + "20200101T00:00:00", + "20200101T000000", + "2020-01-01T00:00:00", + "2020-01-01T00:00:00Z", + "2020-01-01T00:00:00.00", + "2020-01-01T00:00:00.00Z", + "2020-01-01T00:00:00.00+08:30", + ], + ) + def test_is_iso8601_datetime_pass(self, to_validate): + assert validate({"schema": "dateTime"}, to_validate) + + @pytest.mark.parametrize("to_validate", ["P32DT7.592380349524318S", "P32DT7S"]) + def test_is_iso8601_duration_pass(self, to_validate): + assert validate({"schema": "duration"}, to_validate) + + @pytest.mark.parametrize( + "to_validate", ["00:00:00+08:30", "00:00:00Z", "00:00:00.123Z"] + ) + def test_is_iso8601_time_pass(self, to_validate): + assert validate({"schema": "time"}, to_validate) + + # Failure suite + @pytest.mark.parametrize( + "to_validate", ["2020-13-35", *BAD_ARRAY], + ) + def test_is_iso8601_date_fail(self, to_validate): + assert not validate({"schema": "date"}, to_validate) + + @pytest.mark.parametrize("to_validate", ["2020-13-35", "2020-00-00T", *BAD_ARRAY]) + def test_is_iso8601_datetime_fail(self, to_validate): + assert not validate({"schema": "dateTime"}, to_validate) + + @pytest.mark.parametrize("to_validate", ["2020-01", *BAD_ARRAY]) + def test_is_iso8601_duration_fail(self, to_validate): + assert not validate({"schema": "duration"}, to_validate) + + @pytest.mark.parametrize("to_validate", [*BAD_ARRAY]) + def test_is_iso8601_time_fail(self, to_validate): + assert not validate({"schema": "time"}, to_validate) + + +class TestPredefinedComplexType: + @pytest.mark.parametrize( + "value, expected_result", + [ + ({"lat": 123, "lon": 123, "alt": 123}, True), + ({"lat": 123.123, "lon": 123.123, "alt": 123.123}, True), + ({"lat": 123, "lon": 123}, True), + ({"lat": 123}, False), + ({"lon": 123}, False), + ({"alt": 123}, False), + ({"lat": "123.123", "lon": "123.123", "alt": "123.123"}, False), + ({"x": 123, "y": 123, "z": 123}, False), + ({"x": 123.123, "y": 123.123, "z": 123.123}, False), + ], + ) + def test_geopoint(self, value, expected_result): + assert validate({"schema": "geopoint"}, value) == expected_result + + @pytest.mark.parametrize( + "value, expected_result", + [ + ({"x": 123, "y": 123, "z": 123}, True), + ({"x": 123.123, "y": 123.123, "z": 123.123}, True), + ({"lat": 123, "lon": 123, "alt": 123}, False), + ({"lat": 123.123, "lon": 123.123, "alt": 123.123}, False), + ({"lat": 123, "lon": 123}, False), + ({"x": "123", "y": "123", "z": "123"}, False), + ({"x": 123.123, "y": 123.123}, False), + ({"x": 123.123, "z": 123.123}, False), + ({"y": 123.123, "z": 123.123}, False), + ], + ) + def test_vector(self, value, expected_result): + assert validate({"schema": "vector"}, value) == expected_result + + +class TestComplexType: + @pytest.mark.parametrize( + "value, expected_result", + [(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)) + schema = template.get_schema("IntEnum") + assert validate(schema, value) == expected_result + + @pytest.mark.parametrize( + "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)) + schema = template.get_schema("StringEnum") + assert validate(schema, value) == expected_result + + @pytest.mark.parametrize( + "value, expected_result", + [ + ({"Double": 123}, True), + ({"Double": "123"}, False), + ({"double": 123}, False), + ({"asd": 123}, False), + ], + ) + def test_object_simple(self, value, expected_result): + template = Template(load_json(FileNames.central_device_template_file)) + schema = template.get_schema("Object") + assert validate(schema, value) == expected_result + + @pytest.mark.parametrize( + "value, expected_result", + [ + ({"LayerC": {"Depth1C": {"SomeTelemetry": 100}}}, True,), + ({"LayerC": {"Depth1C": {"SomeTelemetry": 100.001}}}, True,), + ({"LayerC": {"Depth1C": {"SomeTelemetry": "100"}}}, False,), + ({"LayerC": {"Depth1C": {"sometelemetry": 100.001}}}, False,), + ({"LayerC": {"depth1c": {"SomeTelemetry": 100.001}}}, False,), + ({"layerc": {"Depth1C": {"SomeTelemetry": 100.001}}}, False,), + ], + ) + def test_object_medium(self, value, expected_result): + template = Template( + load_json(FileNames.central_deeply_nested_device_template_file) + ) + schema = template.get_schema("RidiculousObject") + assert validate(schema, value) == expected_result + + @pytest.mark.parametrize( + "value, expected_result", + [ + ( + { + "LayerA": { + "Depth1A": { + "Depth2": { + "Depth3": { + "Depth4": { + "DeepestComplexEnum": 1, + "DeepestVector": {"x": 1, "y": 2, "z": 3}, + "DeepestGeopoint": { + "lat": 1, + "lon": 2, + "alt": 3, + }, + "Depth5": {"Depth6Double": 123}, + } + } + } + } + } + }, + True, + ), + ( + { + "LayerA": { + "Depth1A": { + "Depth2": { + "Depth3": { + "Depth4": { + "DeepestComplexEnum": 1, + "DeepestVector": {"x": 1, "y": 2, "z": 3}, + "DeepestGeopoint": { + "lat": "1", + "lon": 2, + "alt": 3, + }, + } + } + } + } + } + }, + False, + ), + ( + { + "LayerA": { + "Depth1A": { + "Depth2": { + "Depth3": { + "Depth3": { + "DeepestComplexEnum": 1, + "DeepestVector": {"x": 1, "y": 2, "z": 3}, + "DeepestGeopoint": { + "lat": 1, + "lon": 2, + "alt": 3, + }, + } + } + } + } + } + }, + False, + ), + ], + ) + def test_object_deep(self, value, expected_result): + template = Template( + load_json(FileNames.central_deeply_nested_device_template_file) + ) + schema = template.get_schema("RidiculousObject") + assert validate(schema, value) == expected_result diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index afffce0c6..1b264d8ba 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -15,7 +15,6 @@ process_json_arg, read_file_content, logger, - ISO8601Validator, ensure_min_version, ) from azext_iot.common.deps import ensure_uamqp @@ -281,73 +280,6 @@ def test_file_json_fail_invalidcontent(self, content, argname, set_cwd, mocker): assert mocked_util_logger.call_count == 0 -# none of these are valid anything in ISO 8601 -BAD_ARRAY = ["asd", "", 123.4, 123, True, False] - - -class TestISO8601Validator: - validator = ISO8601Validator() - - # Success suite - @pytest.mark.parametrize( - "to_validate", ["20200101", "20200101Z", "2020-01-01", "2020-01-01Z"] - ) - def test_is_iso8601_date_pass(self, to_validate): - result = self.validator.is_iso8601_date(to_validate) - assert result - - @pytest.mark.parametrize( - "to_validate", - [ - "20200101T00:00:00", - "20200101T000000", - "2020-01-01T00:00:00", - "2020-01-01T00:00:00Z", - "2020-01-01T00:00:00.00", - "2020-01-01T00:00:00.00Z", - "2020-01-01T00:00:00.00+08:30", - ], - ) - def test_is_iso8601_datetime_pass(self, to_validate): - result = self.validator.is_iso8601_datetime(to_validate) - assert result - - @pytest.mark.parametrize("to_validate", ["P32DT7.592380349524318S", "P32DT7S"]) - def test_is_iso8601_duration_pass(self, to_validate): - result = self.validator.is_iso8601_duration(to_validate) - assert result - - @pytest.mark.parametrize( - "to_validate", ["00:00:00+08:30", "00:00:00Z", "00:00:00.123Z"] - ) - def test_is_iso8601_time_pass(self, to_validate): - result = self.validator.is_iso8601_time(to_validate) - assert result - - # Failure suite - @pytest.mark.parametrize( - "to_validate", ["2020-13-35", *BAD_ARRAY], - ) - def test_is_iso8601_date_fail(self, to_validate): - result = self.validator.is_iso8601_date(to_validate) - assert not result - - @pytest.mark.parametrize("to_validate", ["2020-13-35", "2020-00-00T", *BAD_ARRAY]) - def test_is_iso8601_datetime_fail(self, to_validate): - result = self.validator.is_iso8601_datetime(to_validate) - assert not result - - @pytest.mark.parametrize("to_validate", ["2020-01", *BAD_ARRAY]) - def test_is_iso8601_duration_fail(self, to_validate): - result = self.validator.is_iso8601_duration(to_validate) - assert not result - - @pytest.mark.parametrize("to_validate", [*BAD_ARRAY]) - def test_is_iso8601_time_fail(self, to_validate): - result = self.validator.is_iso8601_time(to_validate) - assert not result - - class TestVersionComparison: @pytest.mark.parametrize( "current, minimum, expected", diff --git a/azext_iot/tests/test_monitor_parsers_unit.py b/azext_iot/tests/test_monitor_parsers_unit.py index 1ea29adc6..baf03c71d 100644 --- a/azext_iot/tests/test_monitor_parsers_unit.py +++ b/azext_iot/tests/test_monitor_parsers_unit.py @@ -9,7 +9,12 @@ import pytest from uamqp.message import Message, MessageProperties -from azext_iot.central.providers import CentralDeviceProvider +from azext_iot.central.providers import ( + CentralDeviceProvider, + CentralDeviceTemplateProvider, +) +from azext_iot.central.models.template import Template +from azext_iot.central.models.device import Device 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 @@ -271,12 +276,20 @@ def test_parse_message_bad_field_name_should_fail(self): # parsing should attempt to place raw payload into result even if parsing fails assert parsed_msg["event"]["payload"] == self.bad_field_name + # field name contains '-' character error expected_details_1 = strings.invalid_field_name( list(self.bad_field_name.keys()) ) - _validate_issues(parser, Severity.error, 1, 1, [expected_details_1]) + _validate_issues(parser, Severity.error, 2, 1, [expected_details_1]) - def xtest_validate_against_template_should_fail(self): + # field name not present in template warning + expected_details_2 = strings.invalid_field_name_mismatch_template( + list(self.bad_field_name.keys()), device_template.schema_names + ) + + _validate_issues(parser, Severity.warning, 2, 1, [expected_details_2]) + + def test_validate_against_template_should_fail(self): # setup device_template = self._get_template() @@ -296,9 +309,6 @@ def xtest_validate_against_template_should_fail(self): # act parsed_msg = parser.parse_message() - schema = parser._extract_template_schemas_from_template(device_template) - - schema = parser._extract_template_schemas_from_template(device_template) # verify assert parsed_msg["event"]["payload"] == self.bad_dcm_payload @@ -312,12 +322,12 @@ def xtest_validate_against_template_should_fail(self): assert properties["application"] == self.app_properties expected_details = strings.invalid_field_name_mismatch_template( - list(self.bad_dcm_payload.keys()), list(schema.keys()) + list(self.bad_dcm_payload.keys()), device_template.schema_names ) _validate_issues(parser, Severity.warning, 1, 1, [expected_details]) - def xtest_validate_against_bad_template_should_not_throw(self): + def test_validate_against_bad_template_should_not_throw(self): # setup device_template = "an_unparseable_template" @@ -335,6 +345,11 @@ def xtest_validate_against_bad_template_should_not_throw(self): device_template=device_template, message=message, args=args ) + # haven't found a better way to force the error to occur within parser + parser._central_template_provider.get_device_template = lambda x: Template( + device_template + ) + # act parsed_msg = parser.parse_message() @@ -342,13 +357,13 @@ def xtest_validate_against_bad_template_should_not_throw(self): assert parsed_msg["event"]["payload"] == self.bad_dcm_payload assert parsed_msg["event"]["origin"] == self.device_id - expected_details = strings.invalid_template_extract_schema_failed( - device_template + expected_details = strings.device_template_not_found( + "Could not parse iot central device template." ) _validate_issues(parser, Severity.error, 1, 1, [expected_details]) - def xtest_type_mismatch_should_error(self): + def test_type_mismatch_should_error(self): # setup device_template = self._get_template() @@ -380,18 +395,23 @@ def xtest_type_mismatch_should_error(self): expected_details = strings.invalid_primitive_schema_mismatch_template( field_name, data_type, data ) - _validate_issues(parser, Severity.warning, 1, 1, [expected_details]) + _validate_issues(parser, Severity.error, 1, 1, [expected_details]) def _get_template(self): - return load_json(FileNames.central_device_template_file) + return Template(load_json(FileNames.central_device_template_file)) def _create_parser( - self, device_template: dict, message: Message, args: CommonParserArguments + self, device_template: Template, message: Message, args: CommonParserArguments ): - provider = CentralDeviceProvider(cmd=None, app_id=None) - provider.get_device_template_by_device_id = mock.MagicMock( + 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({})) + template_provider.get_device_template = mock.MagicMock( return_value=device_template ) return central_parser.CentralParser( - message=message, central_device_provider=provider, common_parser_args=args + message=message, + central_device_provider=device_provider, + central_template_provider=template_provider, + common_parser_args=args, ) From 4657fdc7057fdcd157c93f9f9bdbc65a573b6b07 Mon Sep 17 00:00:00 2001 From: valluriraj Date: Tue, 9 Jun 2020 16:44:43 -0700 Subject: [PATCH 050/179] Common Handler - Added ability to filter by edge module name (#198) * add module level filtering - * Revert "add module level filtering -" This reverts commit 340a43c93e7575abaf22f493f3c548795ca02c74. * module filtering help for monitor-events * add module id to iotcentral params * params fixes iotcentral * add params to monitor-events * plumbed module filtering for iothub and dt * add unit tests Co-authored-by: Raj valluri Co-authored-by: prbans --- azext_iot/_params.py | 5 ++ azext_iot/central/_help.py | 20 +++++++- azext_iot/central/commands_device_template.py | 6 +-- azext_iot/central/commands_monitor.py | 4 ++ azext_iot/central/models/template.py | 2 +- azext_iot/central/params.py | 7 +++ azext_iot/monitor/handlers/central_handler.py | 48 +++++++++++-------- azext_iot/monitor/handlers/common_handler.py | 29 +++++++---- azext_iot/monitor/models/arguments.py | 2 + azext_iot/monitor/parsers/common_parser.py | 11 +++++ azext_iot/operations/digitaltwin.py | 2 + azext_iot/operations/hub.py | 6 +++ azext_iot/tests/test_monitor_parsers_unit.py | 26 ++++++++-- 13 files changed, 129 insertions(+), 39 deletions(-) diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 9099e62f1..25f967b49 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -332,6 +332,11 @@ def load_arguments(self, _): with self.argument_context("iot hub monitor-events") as context: context.argument("timeout", arg_type=event_timeout_type) context.argument("properties", arg_type=event_msg_prop_type) + context.argument( + "interface", + options_list=["--interface", "-i"], + help="Target interface name. This should be the name of the interface not the urn-id.", + ) with self.argument_context("iot hub monitor-feedback") as context: context.argument( diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index b6c4e27ac..dd866c0c4 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -259,7 +259,13 @@ def _load_central_monitors_help(): az iot central app monitor-events --app-id {app_id} -d {device_id} - name: Basic usage when filtering targeted devices with a wildcard in the ID text: > - az iot central app monitor-events --app-id {app_id} -d Device* + az iot central app monitor-events --app-id {app_id} -d Device*d + - name: Basic usage when filtering on module. + text: > + az iot central app monitor-events --app-id {app_id} -m {module_id} + - name: Basic usage when filtering targeted modules with a wildcard in the ID + text: > + az iot central app monitor-events --app-id {app_id} -m Module* - name: Filter device and specify an Event Hub consumer group to bind to. text: > az iot central app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} @@ -299,6 +305,12 @@ def _load_central_monitors_help(): - name: Basic usage when filtering targeted devices with a wildcard in the ID text: > az iot central app validate-messages --app-id {app_id} -d Device* + - name: Basic usage when filtering on module. + text: > + az iot central app validate-messages --app-id {app_id} -m {module_id} + - name: Basic usage when filtering targeted modules with a wildcard in the ID + text: > + az iot central app validate-messages --app-id {app_id} -m Module* - name: Filter device and specify an Event Hub consumer group to bind to. text: > az iot central app validate-messages --app-id {app_id} -d {device_id} --cg {consumer_group_name} @@ -328,6 +340,12 @@ def _load_central_deprecated_commands(): - name: Basic usage when filtering targeted devices with a wildcard in the ID text: > az iotcentral app monitor-events --app-id {app_id} -d Device* + - name: Basic usage when filtering on module. + text: > + az iotcentral app monitor-events --app-id {app_id} -m {module_id} + - name: Basic usage when filtering targeted modules with a wildcard in the ID + text: > + az iotcentral app monitor-events --app-id {app_id} -m Module* - name: Filter device and specify an Event Hub consumer group to bind to. text: > az iotcentral app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} diff --git a/azext_iot/central/commands_device_template.py b/azext_iot/central/commands_device_template.py index d75145e1a..9cbcdfab9 100644 --- a/azext_iot/central/commands_device_template.py +++ b/azext_iot/central/commands_device_template.py @@ -23,7 +23,7 @@ def get_device_template( template = provider.get_device_template( device_template_id=device_template_id, central_dns_suffix=central_dns_suffix ) - return template.template + return template.raw_template def list_device_templates( @@ -31,7 +31,7 @@ def list_device_templates( ): provider = CentralDeviceTemplateProvider(cmd=cmd, app_id=app_id, token=token) templates = provider.list_device_templates(central_dns_suffix=central_dns_suffix) - return {template.id: template.template for template in templates.values()} + return {template.id: template.raw_template for template in templates.values()} def map_device_templates( @@ -60,7 +60,7 @@ def create_device_template( payload=payload, central_dns_suffix=central_dns_suffix, ) - return template.template + return template.raw_template def delete_device_template( diff --git a/azext_iot/central/commands_monitor.py b/azext_iot/central/commands_monitor.py index c3953e135..42aa504cf 100644 --- a/azext_iot/central/commands_monitor.py +++ b/azext_iot/central/commands_monitor.py @@ -20,6 +20,7 @@ def validate_messages( cmd: AzCliCommand, app_id, device_id=None, + module_id=None, consumer_group="$Default", timeout=300, enqueued_time=None, @@ -47,6 +48,7 @@ def validate_messages( output=telemetry_args.output, common_parser_args=common_parser_args, device_id=device_id, + module_id=module_id, ) central_handler_args = CentralHandlerArguments( duration=duration, @@ -69,6 +71,7 @@ def monitor_events( cmd: AzCliCommand, app_id, device_id=None, + module_id=None, consumer_group="$Default", timeout=300, enqueued_time=None, @@ -92,6 +95,7 @@ def monitor_events( output=telemetry_args.output, common_parser_args=common_parser_args, device_id=device_id, + module_id=module_id, ) central_handler_args = CentralHandlerArguments( duration=0, diff --git a/azext_iot/central/models/template.py b/azext_iot/central/models/template.py index 6fdfcb992..cbaf2d4f9 100644 --- a/azext_iot/central/models/template.py +++ b/azext_iot/central/models/template.py @@ -9,7 +9,7 @@ class Template: def __init__(self, template: dict): - self.template = template + self.raw_template = template try: self.id = template.get("id") self.name = template.get("displayName") diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 6cef69288..5136ea5c8 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -78,6 +78,9 @@ def load_central_arguments(self, _): help="Central dns suffix. " "This enables running cli commands against non public/prod environments", ) + context.argument( + "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", + ) with self.argument_context("iot central app monitor-events") as context: context.argument("timeout", arg_type=event_timeout_type) @@ -159,6 +162,10 @@ def load_deprecated_iotcentral_params(self, _): "This enables running cli commands against non public/prod environments", ) + context.argument( + "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", + ) + with self.argument_context("iot central device-twin") as context: context.argument("app_id", options_list=["--app-id"], help="Target App.") context.argument( diff --git a/azext_iot/monitor/handlers/central_handler.py b/azext_iot/monitor/handlers/central_handler.py index a491bb55c..d2b277d8b 100644 --- a/azext_iot/monitor/handlers/central_handler.py +++ b/azext_iot/monitor/handlers/central_handler.py @@ -4,18 +4,24 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import csv +import sys + from typing import List +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.monitor.handlers.common_handler import CommonHandler +from azext_iot.monitor.handlers import CommonHandler from azext_iot.monitor.models.arguments import CentralHandlerArguments from azext_iot.monitor.parsers.central_parser import CentralParser from azext_iot.monitor.parsers.issue import Issue +logger = get_logger(__name__) + class CentralHandler(CommonHandler): def __init__( @@ -54,6 +60,9 @@ def validate_message(self, message): if not self._should_process_device(parser.device_id): return + if not self._should_process_module(parser.module_id): + return + parsed_message = parser.parse_message() self._messages.append(parsed_message) @@ -77,30 +86,30 @@ def validate_message(self, message): [issue.log() for issue in issues] def generate_startup_string(self, name: str): - device_filter_text = "" device_id = self._central_handler_args.common_handler_args.device_id + duration = self._central_handler_args.duration + max_messages = self._central_handler_args.max_messages + module_id = self._central_handler_args.common_handler_args.module_id + + filter_text = "" if device_id: - device_filter_text = ".\nFiltering on device: {}".format(device_id) + filter_text = ".\nFiltering on device: {}".format(device_id) + + if module_id: + logger.warn("Module filtering is applicable only for edge devices.") + filter_text += ".\nFiltering on module: {}".format(module_id) exit_text = "" - if ( - self._central_handler_args.duration - and self._central_handler_args.max_messages - ): + if duration and max_messages: exit_text = ".\nExiting after {} second(s), or {} message(s) have been parsed (whichever happens first).".format( - self._central_handler_args.duration, - self._central_handler_args.max_messages, - ) - elif self._central_handler_args.duration: - exit_text = ".\nExiting after {} second(s).".format( - self._central_handler_args.duration - ) - elif self._central_handler_args.max_messages: - exit_text = ".\nExiting after parsing {} message(s).".format( - self._central_handler_args.max_messages + duration, max_messages, ) + elif duration: + exit_text = ".\nExiting after {} second(s).".format(duration) + elif max_messages: + exit_text = ".\nExiting after parsing {} message(s).".format(max_messages) - result = "{} telemetry{}{}".format(name, device_filter_text, exit_text) + result = "{} telemetry{}{}".format(name, filter_text, exit_text) return result @@ -137,9 +146,6 @@ def _handle_json_summary(self, issues: List[Issue]): print(output) def _handle_csv_summary(self, issues: List[Issue]): - import csv - import sys - fieldnames = ["severity", "details", "message", "device_id", "template_id"] writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames) writer.writeheader() diff --git a/azext_iot/monitor/handlers/common_handler.py b/azext_iot/monitor/handlers/common_handler.py index 44903e99a..b779f7d43 100644 --- a/azext_iot/monitor/handlers/common_handler.py +++ b/azext_iot/monitor/handlers/common_handler.py @@ -30,6 +30,9 @@ def parse_message(self, message): if not self._should_process_interface(parser.interface_name): return + if not self._should_process_module(parser.module_id): + return + result = parser.parse_message() if self._common_handler_args.output.lower() == "json": @@ -43,22 +46,24 @@ def _should_process_device(self, device_id): expected_device_id = self._common_handler_args.device_id expected_devices = self._common_handler_args.devices - if expected_device_id and expected_device_id != device_id: - if "*" in expected_device_id or "?" in expected_device_id: + process_device = self._perform_id_match(expected_device_id, device_id) + + if expected_devices and device_id not in expected_devices: + return False + + return process_device + + def _perform_id_match(self, expected_id, actual_id): + if expected_id and expected_id != actual_id: + if "*" in expected_id or "?" in expected_id: regex = ( - re.escape(expected_device_id) - .replace("\\*", ".*") - .replace("\\?", ".") + re.escape(expected_id).replace("\\*", ".*").replace("\\?", ".") + "$" ) - if not re.match(regex, device_id): + if not re.match(regex, actual_id): return False else: return False - - if expected_devices and device_id not in expected_devices: - return False - return True def _should_process_interface(self, interface_name): @@ -70,3 +75,7 @@ def _should_process_interface(self, interface_name): # only process if the expected and actual interface name match return expected_interface_name == interface_name + + def _should_process_module(self, module_id): + expected_module_id = self._common_handler_args.module_id + return self._perform_id_match(expected_module_id, module_id) diff --git a/azext_iot/monitor/models/arguments.py b/azext_iot/monitor/models/arguments.py index adfa297ea..cbe0ef19a 100644 --- a/azext_iot/monitor/models/arguments.py +++ b/azext_iot/monitor/models/arguments.py @@ -49,11 +49,13 @@ def __init__( devices: list = None, device_id="", interface_name="", + module_id="", ): self.output = output self.devices = devices or [] self.device_id = device_id or "" self.interface_name = interface_name or "" + self.module_id = module_id or "" self.common_parser_args = common_parser_args diff --git a/azext_iot/monitor/parsers/common_parser.py b/azext_iot/monitor/parsers/common_parser.py index bc86d0eb1..da64b359e 100644 --- a/azext_iot/monitor/parsers/common_parser.py +++ b/azext_iot/monitor/parsers/common_parser.py @@ -18,6 +18,7 @@ DEVICE_ID_IDENTIFIER = b"iothub-connection-device-id" INTERFACE_NAME_IDENTIFIER = b"iothub-interface-name" +MODULE_ID_IDENTIFIER = b"iothub-connection-module-id" class CommonParser(AbstractBaseParser): @@ -28,6 +29,7 @@ def __init__(self, message: Message, common_parser_args: CommonParserArguments): self.device_id = "" # need to default self.device_id = self._parse_device_id(message) self.interface_name = self._parse_interface_name(message) + self.module_id = self._parse_module_id(message) def parse_message(self) -> dict: message = self._message @@ -37,6 +39,7 @@ def parse_message(self) -> dict: event = {} event["origin"] = self.device_id event["interface"] = self.interface_name + event["module"] = self.module_id if not properties: properties = [] # guard against None being passed in @@ -85,6 +88,14 @@ def _parse_device_id(self, message: Message) -> str: self._add_issue(severity=Severity.error, details=details) return "" + def _parse_module_id(self, message: Message) -> str: + try: + return str(message.annotations.get(MODULE_ID_IDENTIFIER), "utf8") + except Exception: + # a message not containing an module name is expected for non-edge devices + # so there's no "issue" to log here + return "" + def _parse_interface_name(self, message: Message) -> str: try: return str(message.annotations.get(INTERFACE_NAME_IDENTIFIER), "utf8") diff --git a/azext_iot/operations/digitaltwin.py b/azext_iot/operations/digitaltwin.py index 249ca1b58..c394a7315 100644 --- a/azext_iot/operations/digitaltwin.py +++ b/azext_iot/operations/digitaltwin.py @@ -283,6 +283,7 @@ def iot_digitaltwin_monitor_events( content_type=None, device_query=None, interface=None, + module_id=None, ): _iot_hub_monitor_events( cmd=cmd, @@ -299,6 +300,7 @@ def iot_digitaltwin_monitor_events( content_type=content_type, device_query=device_query, interface_name=interface, + module_id=module_id, ) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 89f5f7a7b..3bf0a7dce 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -2185,6 +2185,8 @@ def iot_hub_monitor_events( cmd, hub_name=None, device_id=None, + interface=None, + module_id=None, consumer_group="$Default", timeout=300, enqueued_time=None, @@ -2201,6 +2203,8 @@ def iot_hub_monitor_events( cmd, hub_name=hub_name, device_id=device_id, + interface_name=interface, + module_id=module_id, consumer_group=consumer_group, timeout=timeout, enqueued_time=enqueued_time, @@ -2259,6 +2263,7 @@ def iot_hub_distributed_tracing_show( def _iot_hub_monitor_events( cmd, interface_name=None, + module_id=None, hub_name=None, device_id=None, consumer_group="$Default", @@ -2312,6 +2317,7 @@ def _iot_hub_monitor_events( devices=device_ids, device_id=device_id, interface_name=interface_name, + module_id=module_id, ) handler = CommonHandler(handler_args) diff --git a/azext_iot/tests/test_monitor_parsers_unit.py b/azext_iot/tests/test_monitor_parsers_unit.py index baf03c71d..0dd3bb31f 100644 --- a/azext_iot/tests/test_monitor_parsers_unit.py +++ b/azext_iot/tests/test_monitor_parsers_unit.py @@ -55,13 +55,14 @@ class TestCommonParser: bad_content_type = "bad-content-type" @pytest.mark.parametrize( - "device_id, encoding, content_type, interface_name, payload, properties, app_properties", + "device_id, encoding, content_type, interface_name, module_id, payload, properties, app_properties", [ ( "device-id", "utf-8", "application/json", "interface_name", + "module-id", {"payloadKey": "payloadValue"}, {"propertiesKey": "propertiesValue"}, {"appPropsKey": "appPropsValue"}, @@ -70,6 +71,17 @@ class TestCommonParser: "device-id", "utf-8", "application/json", + "interface_name", + "", + {"payloadKey": "payloadValue"}, + {"propertiesKey": "propertiesValue"}, + {"appPropsKey": "appPropsValue"}, + ), + ( + "device-id", + "utf-8", + "application/json", + "", "", {"payloadKey": "payloadValue"}, {"propertiesKey": "propertiesValue"}, @@ -80,6 +92,7 @@ class TestCommonParser: "utf-8", "application/json", "", + "", {}, {"propertiesKey": "propertiesValue"}, {"appPropsKey": "appPropsValue"}, @@ -89,11 +102,12 @@ class TestCommonParser: "utf-8", "application/json", "", + "", {}, {}, {"appPropsKey": "appPropsValue"}, ), - ("device-id", "utf-8", "application/json", "", {}, {}, {},), + ("device-id", "utf-8", "application/json", "", "", {}, {}, {},), ], ) def test_parse_message_should_succeed( @@ -105,6 +119,7 @@ def test_parse_message_should_succeed( payload, properties, app_properties, + module_id, ): # setup properties = MessageProperties( @@ -116,6 +131,7 @@ def test_parse_message_should_succeed( annotations={ common_parser.DEVICE_ID_IDENTIFIER: device_id.encode(), common_parser.INTERFACE_NAME_IDENTIFIER: interface_name.encode(), + common_parser.MODULE_ID_IDENTIFIER: module_id.encode(), }, application_properties=_encode_app_props(app_properties), ) @@ -130,7 +146,11 @@ def test_parse_message_should_succeed( assert parsed_msg["event"]["origin"] == device_id device_identifier = str(common_parser.DEVICE_ID_IDENTIFIER, "utf8") assert parsed_msg["event"]["annotations"][device_identifier] == device_id - + module_identifier = str(common_parser.MODULE_ID_IDENTIFIER, "utf8") + if module_id: + assert parsed_msg["event"]["annotations"][module_identifier] == module_id + else: + assert not parsed_msg["event"]["annotations"].get(module_identifier) properties = parsed_msg["event"]["properties"] assert properties["system"]["content_encoding"] == encoding assert properties["system"]["content_type"] == content_type From f3fa54541ed420d6e86dc961754791dded9f9747 Mon Sep 17 00:00:00 2001 From: Paul Montgomery Date: Wed, 10 Jun 2020 09:46:53 -0700 Subject: [PATCH 051/179] More consistent twin-id argument (#199) --- azext_iot/digitaltwins/_help.py | 4 ++-- azext_iot/digitaltwins/commands_twins.py | 4 ++-- azext_iot/digitaltwins/params.py | 22 ++++++++++--------- azext_iot/digitaltwins/providers/twin.py | 4 ++-- .../test_dt_twin_lifecycle_int.py | 2 +- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index 13ccb05f8..f3e09b1b5 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -383,12 +383,12 @@ def load_digitaltwins_help(): - name: Create a relationship between two digital twins. text: > az dt twin relationship create -n {instance_name} --relationship-id {relationship_id} --relationship contains - --source {source_twin_id} --target {target_twin_id} + --twin-id {source_twin_id} --target {target_twin_id} - name: Create a relationship with initialized properties between two digital twins. text: > az dt twin relationship create -n {instance_name} --relationship-id {relationship_id} --relationship contains - --source {source_twin_id} --target {target_twin_id} + --twin-id {source_twin_id} --target {target_twin_id} --properties '{"ownershipUser": "me", "ownershipDepartment": "Computer Science"}' """ diff --git a/azext_iot/digitaltwins/commands_twins.py b/azext_iot/digitaltwins/commands_twins.py index c7a4eaba7..f924ad00e 100644 --- a/azext_iot/digitaltwins/commands_twins.py +++ b/azext_iot/digitaltwins/commands_twins.py @@ -42,7 +42,7 @@ def delete_twin(cmd, name, twin_id, resource_group_name=None): def create_relationship( cmd, name, - source_twin_id, + twin_id, target_twin_id, relationship_id, relationship, @@ -51,7 +51,7 @@ def create_relationship( ): twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) return twin_provider.add_relationship( - source_twin_id=source_twin_id, + twin_id=twin_id, target_twin_id=target_twin_id, relationship_id=relationship_id, relationship=relationship, diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index ad8dc44c3..fc22bf936 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -183,16 +183,6 @@ def load_digitaltwins_arguments(self, _): options_list=["--relationship-id", "-r"], help="Relationship Id.", ) - context.argument( - "source_twin_id", - options_list=["--source-twin-id", "--source", "-s"], - help="The source twin Id for a relationship.", - ) - context.argument( - "target_twin_id", - options_list=["--target-twin-id", "--target", "-t"], - help="The target twin Id for a relationship.", - ) context.argument( "relationship", options_list=["--relationship", "--kind"], @@ -237,6 +227,18 @@ def load_digitaltwins_arguments(self, _): help="The path to the DTDL component. If set, telemetry will be emitted on behalf of the component.", ) + with self.argument_context("dt twin relationship") as context: + context.argument( + "twin_id", + options_list=["--twin-id", "-t"], + help="The source twin Id for a relationship.", + ) + context.argument( + "target_twin_id", + options_list=["--target-twin-id", "--target"], + help="The target twin Id for a relationship.", + ) + with self.argument_context("dt twin relationship create") as context: context.argument( "properties", diff --git a/azext_iot/digitaltwins/providers/twin.py b/azext_iot/digitaltwins/providers/twin.py index 80d72681c..821ac214c 100644 --- a/azext_iot/digitaltwins/providers/twin.py +++ b/azext_iot/digitaltwins/providers/twin.py @@ -98,7 +98,7 @@ def delete(self, twin_id): def add_relationship( self, - source_twin_id, + twin_id, target_twin_id, relationship_id, relationship, @@ -117,7 +117,7 @@ def add_relationship( logger.info("Relationship payload %s", json.dumps(relationship_request)) return self.twins_sdk.add_relationship( - id=source_twin_id, + id=twin_id, relationship_id=relationship_id, relationship=relationship_request, if_none_match="*", 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 ee7768354..2546577cb 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -207,7 +207,7 @@ def test_dt_twin(self): ) twin_relationship_create_result = self.cmd( - "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --source-twin-id {} " + "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --twin-id {} " "--target-twin-id {} --properties '{}'".format( self.instance_name, self.group_names[0], From c07dd415c4b3569232268b1180cbc4415b78641e Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 15 Jun 2020 13:41:53 -0700 Subject: [PATCH 052/179] Adds subscription option for DT endpoint create. (#201) * Tweak to CI unit test execution. Execute tests based on pattern. * Upped timeout values for endpoints and removed kwarg from command layer. * Re-added "--source" for relationship source twin, for those that want to use "--source" & "--target" conventions. --- .azure-devops/templates/run-tests.yml | 19 ++------ .../templates/set-testenv-sentinel.yml | 28 +++++++++++ azext_iot/common/embedded_cli.py | 17 +++++-- azext_iot/constants.py | 2 +- azext_iot/digitaltwins/commands_resource.py | 18 ++++--- azext_iot/digitaltwins/params.py | 23 ++++++++- azext_iot/digitaltwins/providers/resource.py | 16 +++++-- azext_iot/tests/test_iot_utility_unit.py | 47 +++++++++++++++++++ 8 files changed, 137 insertions(+), 33 deletions(-) create mode 100644 .azure-devops/templates/set-testenv-sentinel.yml diff --git a/.azure-devops/templates/run-tests.yml b/.azure-devops/templates/run-tests.yml index 876d7963f..d9e164234 100644 --- a/.azure-devops/templates/run-tests.yml +++ b/.azure-devops/templates/run-tests.yml @@ -28,22 +28,9 @@ steps: displayName: 'Execute all Tests' - ${{ if eq(parameters.runUnitTestsOnly, 'true') }}: - - script: pytest -v azext_iot/tests/test_iot_ext_unit.py --junitxml=junit/test-iothub-unit-results.xml - displayName: 'Execute IoT Hub unit tests' - - script: pytest -v azext_iot/tests/test_iot_dps_unit.py --junitxml=junit/test-dps-unit-results.xml - displayName: 'Execute DPS unit tests' - - script: pytest -v azext_iot/tests/test_iot_utility_unit.py --junitxml=junit/test-utility-unit-results.xml - displayName: 'Execute Utility unit tests' - - script: pytest -v azext_iot/tests/test_iot_central_unit.py --junitxml=junit/test-central-unit-results.xml - displayName: 'Execute IoT Central unit tests' - - script: pytest -v azext_iot/tests/test_iot_pnp_unit.py --junitxml=junit/test-pnp-unit-results.xml - displayName: 'Execute IoT PnP unit tests' - - script: pytest -v azext_iot/tests/test_iot_digitaltwin_unit.py --junitxml=junit/test-dt-unit-results.xml - displayName: 'Execute IoT DigitalTwin unit tests' - - script: pytest -v azext_iot/tests/iothub/configurations/test_iot_config_unit.py --junitxml=junit/test-config-unit-results.xml - displayName: 'Execute IoT Configuration unit tests' - - script: pytest -v azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py --junitxml=junit/test-jobs-unit-results.xml - displayName: 'Execute IoT Hub job unit tests' + - template: set-testenv-sentinel.yml + - script: pytest -vv azext_iot/tests/ -k "_unit" --junitxml=junit/test-iotext-unit-results.xml + displayName: 'Execute IoT extension unit tests' - task: PublishTestResults@2 condition: succeededOrFailed() diff --git a/.azure-devops/templates/set-testenv-sentinel.yml b/.azure-devops/templates/set-testenv-sentinel.yml new file mode 100644 index 000000000..79f477681 --- /dev/null +++ b/.azure-devops/templates/set-testenv-sentinel.yml @@ -0,0 +1,28 @@ +steps: + - task: PythonScript@0 + displayName : 'Set test envs with sentinel values' + name: 'setTestEnvSentinel' + inputs: + scriptSource: 'inline' + script: | + # This task is in place to get around DevOps pipelines env var naming rules which would require + # application code changes. + sentinel_value = "sentinel" + envvars = [ + "azext_iot_testhub", + "azext_iot_testhub_cs", + "azext_iot_testrg", + "azext_iot_testdps", + "azext_pnp_endpoint", + "azext_pnp_repository", + "azext_pnp_cs", + "azext_iot_central_app_id", + "azext_iot_central_device_template_path", + ] + f = open("./pytest.ini", "w+") + f.write("[pytest]\n") + f.write("junit_family = xunit1\n") + f.write("env = \n") + envvars_sentinel = [" {}={}\n".format(envvar, sentinel_value) for envvar in envvars] + f.writelines(envvars_sentinel) + f.close() diff --git a/azext_iot/common/embedded_cli.py b/azext_iot/common/embedded_cli.py index cbe96b926..2fa7a8826 100644 --- a/azext_iot/common/embedded_cli.py +++ b/azext_iot/common/embedded_cli.py @@ -21,10 +21,16 @@ def __init__(self): self.error_code = 0 self.az_cli = get_default_cli() - def invoke(self, command): + def invoke(self, command: str, subscription: str = None): output_file = StringIO() - command = self._ensure_json_output(command) - self.error_code = self.az_cli.invoke(shlex.split(command), out_file=output_file) or 0 + command = self._ensure_json_output(command=command) + if subscription: + command = self._ensure_subscription( + command=command, subscription=subscription + ) + self.error_code = ( + self.az_cli.invoke(shlex.split(command), out_file=output_file) or 0 + ) self.output = output_file.getvalue() logger.debug( "Embedded CLI received error code: %s, output: '%s'", @@ -49,5 +55,8 @@ def success(self): logger.debug("Operation error code: %s", self.error_code) return self.error_code == 0 - def _ensure_json_output(self, command): + def _ensure_json_output(self, command: str): return "{} -o json".format(command) + + def _ensure_subscription(self, command: str, subscription: str): + return "{} --subscription '{}'".format(command, subscription) diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 4815234c6..e6b48712c 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.4" +VERSION = "0.9.5" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/digitaltwins/commands_resource.py b/azext_iot/digitaltwins/commands_resource.py index 40d49add0..c2454e35a 100644 --- a/azext_iot/digitaltwins/commands_resource.py +++ b/azext_iot/digitaltwins/commands_resource.py @@ -62,6 +62,7 @@ def add_endpoint_eventgrid( eventgrid_topic_name, eventgrid_resource_group, resource_group_name=None, + endpoint_subscription=None, tags=None, ): return _add_endpoint_eventgrid( @@ -71,6 +72,7 @@ def add_endpoint_eventgrid( eventgrid_resource_group=eventgrid_resource_group, eventgrid_topic_name=eventgrid_topic_name, resource_group_name=resource_group_name, + endpoint_subscription=endpoint_subscription, tags=tags, ) @@ -81,8 +83,8 @@ def _add_endpoint_eventgrid( endpoint_name, eventgrid_topic_name, eventgrid_resource_group, - timeout=15, resource_group_name=None, + endpoint_subscription=None, tags=None, ): rp = ResourceProvider(cmd) @@ -93,8 +95,8 @@ def _add_endpoint_eventgrid( endpoint_resource_type=ADTEndpointType.eventgridtopic, endpoint_resource_name=eventgrid_topic_name, endpoint_resource_group=eventgrid_resource_group, + endpoint_subscription=endpoint_subscription, tags=tags, - timeout=timeout, ) @@ -107,6 +109,7 @@ def add_endpoint_servicebus( servicebus_policy, servicebus_namespace, resource_group_name=None, + endpoint_subscription=None, tags=None, ): return _add_endpoint_servicebus( @@ -118,6 +121,7 @@ def add_endpoint_servicebus( servicebus_policy=servicebus_policy, servicebus_namespace=servicebus_namespace, resource_group_name=resource_group_name, + endpoint_subscription=endpoint_subscription, tags=tags, ) @@ -130,8 +134,8 @@ def _add_endpoint_servicebus( servicebus_resource_group, servicebus_policy, servicebus_namespace, - timeout=15, resource_group_name=None, + endpoint_subscription=None, tags=None, ): rp = ResourceProvider(cmd) @@ -144,8 +148,8 @@ def _add_endpoint_servicebus( endpoint_resource_group=servicebus_resource_group, endpoint_resource_namespace=servicebus_namespace, endpoint_resource_policy=servicebus_policy, + endpoint_subscription=endpoint_subscription, tags=tags, - timeout=timeout, ) @@ -158,6 +162,7 @@ def add_endpoint_eventhub( eventhub_policy, eventhub_namespace, resource_group_name=None, + endpoint_subscription=None, tags=None, ): return _add_endpoint_eventhub( @@ -169,6 +174,7 @@ def add_endpoint_eventhub( eventhub_policy=eventhub_policy, eventhub_namespace=eventhub_namespace, resource_group_name=resource_group_name, + endpoint_subscription=endpoint_subscription, tags=tags, ) @@ -181,8 +187,8 @@ def _add_endpoint_eventhub( eventhub_resource_group, eventhub_policy, eventhub_namespace, - timeout=15, resource_group_name=None, + endpoint_subscription=None, tags=None, ): rp = ResourceProvider(cmd) @@ -195,6 +201,6 @@ def _add_endpoint_eventhub( endpoint_resource_group=eventhub_resource_group, endpoint_resource_namespace=eventhub_namespace, endpoint_resource_policy=eventhub_policy, + endpoint_subscription=endpoint_subscription, tags=tags, - timeout=timeout, ) diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index fc22bf936..4c25a7c9b 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -113,6 +113,13 @@ def load_digitaltwins_arguments(self, _): help="Name of EventGrid Topic resource group.", arg_group="Event Grid Topic", ) + context.argument( + "endpoint_subscription", + options_list=["--eventgrid-subscription", "--egs"], + help="Name or ID of subscription where the endpoint resource exists. " + "If no subscription is provided the default subscription is used.", + arg_group="Event Grid Topic", + ) with self.argument_context("dt endpoint create eventhub") as context: context.argument( @@ -139,6 +146,13 @@ def load_digitaltwins_arguments(self, _): help="Name of EventHub resource group.", arg_group="Event Hub", ) + context.argument( + "endpoint_subscription", + options_list=["--eventhub-subscription", "--ehs"], + help="Name or ID of subscription where the endpoint resource exists. " + "If no subscription is provided the default subscription is used.", + arg_group="Event Hub", + ) with self.argument_context("dt endpoint create servicebus") as context: context.argument( @@ -165,6 +179,13 @@ def load_digitaltwins_arguments(self, _): help="Name of ServiceBus resource group.", arg_group="Service Bus Topic", ) + context.argument( + "endpoint_subscription", + options_list=["--servicebus-subscription", "--sbs"], + help="Name or ID of subscription where the endpoint resource exists. " + "If no subscription is provided the default subscription is used.", + arg_group="Service Bus Topic", + ) with self.argument_context("dt twin") as context: context.argument( @@ -230,7 +251,7 @@ def load_digitaltwins_arguments(self, _): with self.argument_context("dt twin relationship") as context: context.argument( "twin_id", - options_list=["--twin-id", "-t"], + options_list=["--twin-id", "-t", "--source"], help="The source twin Id for a relationship.", ) context.argument( diff --git a/azext_iot/digitaltwins/providers/resource.py b/azext_iot/digitaltwins/providers/resource.py index fdaf23ef0..de15856c2 100644 --- a/azext_iot/digitaltwins/providers/resource.py +++ b/azext_iot/digitaltwins/providers/resource.py @@ -20,7 +20,7 @@ def __init__(self, cmd): self.mgmt_sdk = self.get_mgmt_sdk() self.rbac = RbacProvider() - def create(self, name, resource_group_name, location, tags=None, timeout=15): + def create(self, name, resource_group_name, location, tags=None, timeout=20): if tags: tags = validate_key_value_pairs(tags) @@ -207,6 +207,7 @@ def delete_endpoint(self, name, endpoint_name, resource_group_name=None): except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) + # TODO: Breakout and refactor def add_endpoint( self, name, @@ -216,6 +217,7 @@ def add_endpoint( endpoint_resource_group, endpoint_resource_policy=None, endpoint_resource_namespace=None, + endpoint_subscription=None, tags=None, resource_group_name=None, timeout=20, @@ -255,7 +257,8 @@ def add_endpoint( eg_topic_keys_op = cli.invoke( "eventgrid topic key list -n {} -g {}".format( endpoint_resource_name, endpoint_resource_group - ) + ), + subscription=endpoint_subscription ) if not eg_topic_keys_op.success(): raise CLIError("{} Event Grid topic keys.".format(error_prefix)) @@ -264,7 +267,8 @@ def add_endpoint( eg_topic_endpoint_op = cli.invoke( "eventgrid topic show -n {} -g {}".format( endpoint_resource_name, endpoint_resource_group - ) + ), + subscription=endpoint_subscription ) if not eg_topic_endpoint_op.success(): raise CLIError("{} Event Grid topic endpoint.".format(error_prefix)) @@ -283,7 +287,8 @@ def add_endpoint( endpoint_resource_namespace, endpoint_resource_group, endpoint_resource_name, - ) + ), + subscription=endpoint_subscription ) if not sb_topic_keys_op.success(): raise CLIError("{} Service Bus topic keys.".format(error_prefix)) @@ -305,7 +310,8 @@ def add_endpoint( endpoint_resource_namespace, endpoint_resource_group, endpoint_resource_name, - ) + ), + subscription=endpoint_subscription ) if not eventhub_topic_keys_op.success(): raise CLIError("{} Event Hub keys.".format(error_prefix)) diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index 1b264d8ba..215e52eb6 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -20,6 +20,7 @@ from azext_iot.common.deps import ensure_uamqp from azext_iot.constants import EVENT_LIB, EXTENSION_NAME from azext_iot._validators import mode2_iot_login_handler +from azext_iot.common.embedded_cli import EmbeddedCLI class TestMinPython(object): @@ -294,3 +295,49 @@ class TestVersionComparison: ) def test_ensure_min_version(self, current, minimum, expected): assert ensure_min_version(current, minimum) == expected + + +class TestEmbeddedCli: + @pytest.fixture(params=[0, 1]) + def mocked_azclient(self, mocker, request): + def mock_invoke(args, out_file): + out_file.write(json.dumps({"generickey": "genericvalue"})) + return request.param + + azclient = mocker.patch("azext_iot.common.embedded_cli.get_default_cli") + azclient.return_value.invoke.side_effect = mock_invoke + azclient.test_meta.error_code = request.param + return azclient + + @pytest.mark.parametrize( + "command, subscription", + [ + ("iot hub device-identity create -n abcd -d dcba", None), + ("iot hub device-twin show -n 'abcd' -d 'dcba'", "20a300e5-a444-4130-bb5a-1abd08ad930a"), + ], + ) + def test_embedded_cli(self, mocked_azclient, command, subscription): + import shlex + + cli = EmbeddedCLI() + cli.invoke(command=command, subscription=subscription) + + # Due to forced json output + command += " -o json" + + if subscription: + command += " --subscription '{}'".format(subscription) + + expected_args = shlex.split(command) + call = mocked_azclient().invoke.call_args_list[0] + actual_args, _ = call + assert expected_args == actual_args[0] + success = cli.success() + + if mocked_azclient.test_meta.error_code == 1: + assert not success + elif mocked_azclient.test_meta.error_code == 0: + assert success + + assert cli.output + assert cli.as_json() From 962f26f511c18c05e7a91c0378116103097faa8c Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 18 Jun 2020 11:36:03 -0700 Subject: [PATCH 053/179] Staging point. (#202) --- azext_iot/common/_azure.py | 127 --------- azext_iot/digitaltwins/_help.py | 2 +- azext_iot/iothub/mgmt_helpers.py | 28 -- .../conftest.py => iothub/models/__init__.py} | 9 - azext_iot/iothub/models/iothub_target.py | 33 +++ azext_iot/iothub/providers/base.py | 8 +- azext_iot/iothub/providers/discovery.py | 170 +++++++++++ azext_iot/operations/digitaltwin.py | 27 +- azext_iot/operations/hub.py | 267 +++++++++++------- azext_iot/tests/conftest.py | 24 +- .../configurations/test_iot_config_unit.py | 94 +++--- azext_iot/tests/iothub/jobs/conftest.py | 19 -- .../iothub/jobs/test_iothub_jobs_unit.py | 40 +-- azext_iot/tests/test_iot_digitaltwin_unit.py | 4 +- azext_iot/tests/test_iot_ext_unit.py | 178 +----------- 15 files changed, 473 insertions(+), 557 deletions(-) delete mode 100644 azext_iot/iothub/mgmt_helpers.py rename azext_iot/{tests/digitaltwins/conftest.py => iothub/models/__init__.py} (66%) create mode 100644 azext_iot/iothub/models/iothub_target.py create mode 100644 azext_iot/iothub/providers/discovery.py delete mode 100644 azext_iot/tests/iothub/jobs/conftest.py diff --git a/azext_iot/common/_azure.py b/azext_iot/common/_azure.py index 0c5656c4e..781f15534 100644 --- a/azext_iot/common/_azure.py +++ b/azext_iot/common/_azure.py @@ -6,8 +6,6 @@ from knack.util import CLIError from azext_iot.common.utility import validate_key_value_pairs -from azext_iot.common.utility import trim_from_start -from azext_iot._factory import iot_hub_service_factory from azext_iot.common.auth import get_aad_token @@ -48,131 +46,6 @@ def parse_iot_device_module_connection_string(cs): CONN_STR_TEMPLATE = "HostName={};SharedAccessKeyName={};SharedAccessKey={}" -def get_iot_hub_connection_string( - cmd, - hub_name, - resource_group_name, - policy_name="iothubowner", - key_type="primary", - include_events=False, - login=None, -): - """ - Function used to build up dictionary of IoT Hub connection string parts - - Args: - cmd (object): Knack cmd - hub_name (str): IoT Hub name - resource_group_name (str): name of Resource Group contianing IoT Hub - policy_name (str): Security policy name for shared key; e.g. 'iothubowner'(default) - key_type (str): Shared key; either 'primary'(default) or 'secondary' - include_events (bool): Include key event hub properties - - Returns: - (dict): of connection string elements. - - Raises: - CLIError: on input validation failure. - - """ - - result = {} - target_hub = None - policy = None - - if login: - try: - decomposed = parse_iot_hub_connection_string(login) - decomposed_lower = dict((k.lower(), v) for k, v in decomposed.items()) - except ValueError as e: - raise CLIError(e) - - result = {} - result["cs"] = login - result["policy"] = decomposed_lower["sharedaccesskeyname"] - result["primarykey"] = decomposed_lower["sharedaccesskey"] - result["entity"] = decomposed_lower["hostname"] - return result - - client = None - if getattr(cmd, "cli_ctx", None): - client = iot_hub_service_factory(cmd.cli_ctx) - else: - client = cmd - - def _find_iot_hub_from_list(hubs, hub_name): - if hubs: - return next( - (hub for hub in hubs if hub_name.lower() == hub.name.lower()), None - ) - return None - - if resource_group_name is None: - hubs = client.list_by_subscription() - if not hubs: - raise CLIError("No IoT Hub found in current subscription.") - target_hub = _find_iot_hub_from_list(hubs, hub_name) - else: - try: - target_hub = client.get(resource_group_name, hub_name) - except Exception: - pass - - if target_hub is None: - raise CLIError( - "No IoT Hub found with name {} in current subscription.".format(hub_name) - ) - - try: - addprops = getattr(target_hub, "additional_properties", None) - resource_group_name = ( - addprops.get("resourcegroup") - if addprops - else getattr(target_hub, "resourcegroup", None) - ) - - policy = client.get_keys_for_key_name( - resource_group_name, target_hub.name, policy_name - ) - except Exception: - pass - - if policy is None: - raise CLIError( - "No keys found for policy {} of IoT Hub {}.".format(policy_name, hub_name) - ) - - result["cs"] = CONN_STR_TEMPLATE.format( - target_hub.properties.host_name, - policy.key_name, - policy.primary_key if key_type == "primary" else policy.secondary_key, - ) - result["entity"] = target_hub.properties.host_name - result["policy"] = policy_name - result["primarykey"] = policy.primary_key - result["secondarykey"] = policy.secondary_key - result["subscription"] = client.config.subscription_id - result["resourcegroup"] = resource_group_name - result["location"] = target_hub.location - result["sku_tier"] = target_hub.sku.tier.value - - if include_events: - events = {} - events["endpoint"] = trim_from_start( - target_hub.properties.event_hub_endpoints["events"].endpoint, "sb://" - ).strip("/") - events["partition_count"] = target_hub.properties.event_hub_endpoints[ - "events" - ].partition_count - events["path"] = target_hub.properties.event_hub_endpoints["events"].path - events["partition_ids"] = target_hub.properties.event_hub_endpoints[ - "events" - ].partition_ids - result["events"] = events - - return result - - def get_iot_dps_connection_string( client, dps_name, diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index f3e09b1b5..be81f21f8 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -14,7 +14,7 @@ def load_digitaltwins_help(): helps["dt"] = """ type: group - short-summary: Manage Azure Digital Twins (ADT) solutions & infrastructure. + short-summary: Manage Azure Digital Twins solutions & infrastructure. """ helps["dt create"] = """ diff --git a/azext_iot/iothub/mgmt_helpers.py b/azext_iot/iothub/mgmt_helpers.py deleted file mode 100644 index a9a6bf4c9..000000000 --- a/azext_iot/iothub/mgmt_helpers.py +++ /dev/null @@ -1,28 +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._factory import iot_hub_service_factory -from knack.util import CLIError - - -def get_mgmt_iothub_exception(): - """ Py 2/3 compatible management plane exception import""" - - try: - from azure.mgmt.iothub.models.error_details_py3 import ErrorDetailsException - except ImportError: - from azure.mgmt.iothub.models.error_details import ErrorDetailsException - - return ErrorDetailsException - - -def get_mgmt_iothub_client(cmd, raise_if_error=False): - try: - return iot_hub_service_factory(cmd.cli_ctx) - except CLIError as e: - if raise_if_error: - raise e - return None diff --git a/azext_iot/tests/digitaltwins/conftest.py b/azext_iot/iothub/models/__init__.py similarity index 66% rename from azext_iot/tests/digitaltwins/conftest.py rename to azext_iot/iothub/models/__init__.py index fd4d03453..55614acbf 100644 --- a/azext_iot/tests/digitaltwins/conftest.py +++ b/azext_iot/iothub/models/__init__.py @@ -3,12 +3,3 @@ # 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.digitaltwins.providers.resource import ResourceProvider - - -@pytest.fixture -def dt_rp(fixture_cmd2): - rp = ResourceProvider(fixture_cmd2) - yield rp diff --git a/azext_iot/iothub/models/iothub_target.py b/azext_iot/iothub/models/iothub_target.py new file mode 100644 index 000000000..798342203 --- /dev/null +++ b/azext_iot/iothub/models/iothub_target.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.common._azure import parse_iot_hub_connection_string + + +# TODO: Align with vNext for IoT Hub +class IotHubTarget: + def __init__(self, decomposed: dict): + # Revisit + decomposed_lower = dict((k.lower(), v) for k, v in decomposed.items()) + + self.cs = decomposed_lower.get("cs") + self.policy = decomposed_lower.get("sharedaccesskeyname") + self.shared_access_key = decomposed_lower.get("sharedaccesskey") + self.entity = decomposed_lower.get("hostname") + + @classmethod + def from_connection_string(cls, cstring): + decomposed = parse_iot_hub_connection_string(cs=cstring) + decomposed["cs"] = cstring + return cls(decomposed) + + def as_dict(self): + return { + "cs": self.cs, + "policy": self.policy, + "primarykey": self.shared_access_key, + "entity": self.entity, + } diff --git a/azext_iot/iothub/providers/base.py b/azext_iot/iothub/providers/base.py index 408d9e9aa..cac0d7e5b 100644 --- a/azext_iot/iothub/providers/base.py +++ b/azext_iot/iothub/providers/base.py @@ -4,7 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_iot.common._azure import get_iot_hub_connection_string +from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot._factory import SdkResolver from msrest.exceptions import SerializationError from msrestazure.azure_exceptions import CloudError @@ -18,10 +18,10 @@ def __init__(self, cmd, hub_name, rg, login=None): self.cmd = cmd self.hub_name = hub_name self.rg = rg - self.target = get_iot_hub_connection_string( - cmd=self.cmd, + self.discovery = IotHubDiscovery(cmd) + self.target = self.discovery.get_target( hub_name=self.hub_name, - resource_group_name=self.rg, + rg=self.rg, login=login, ) self.resolver = SdkResolver(self.target) diff --git a/azext_iot/iothub/providers/discovery.py b/azext_iot/iothub/providers/discovery.py new file mode 100644 index 000000000..195388e8b --- /dev/null +++ b/azext_iot/iothub/providers/discovery.py @@ -0,0 +1,170 @@ +# 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 +from knack.log import get_logger +from azext_iot.common.utility import trim_from_start +from azext_iot.iothub.models.iothub_target import IotHubTarget +from azext_iot._factory import iot_hub_service_factory +from typing import Dict + +PRIVILEDGED_ACCESS_RIGHTS_SET = set( + ["RegistryWrite", "ServiceConnect", "DeviceConnect"] +) +CONN_STR_TEMPLATE = "HostName={};SharedAccessKeyName={};SharedAccessKey={}" + +logger = get_logger(__name__) + + +# TODO: Consider abstract base class +class IotHubDiscovery(object): + def __init__(self, cmd): + self.cmd = cmd + self.client = None + self.sub_id = "unknown" + + def _self_initialize_client(self): + if not self.client: + if getattr(self.cmd, "cli_ctx", None): + self.client = iot_hub_service_factory(self.cmd.cli_ctx) + else: + self.client = self.cmd + self.sub_id = self.client.config.subscription_id + + def find_iothub(self, hub_name, rg=None): + from azure.mgmt.iothub.models import ErrorDetailsException + self._self_initialize_client() + + if rg: + try: + return self.client.get(resource_group_name=rg, resource_name=hub_name) + except ErrorDetailsException: + raise CLIError( + "Unable to find IoT Hub: {} in resource group: {}".format( + hub_name, rg + ) + ) + + hubs_pager = self.client.list_by_subscription() + hubs_list = [] + + try: + while True: + hubs_list.extend(hubs_pager.advance_page()) + except StopIteration: + pass + + if hubs_list: + target_hub = next( + (hub for hub in hubs_list if hub_name.lower() == hub.name.lower()), None + ) + if target_hub: + return target_hub + + raise CLIError( + "Unable to find IoT Hub: {} in current subscription {}.".format( + hub_name, self.sub_id + ) + ) + + def find_policy(self, hub_name, rg, policy_name="auto"): + self._self_initialize_client() + + if policy_name.lower() != "auto": + return self.client.get_keys_for_key_name( + resource_group_name=rg, resource_name=hub_name, key_name=policy_name + ) + + policy_pager = self.client.list_keys( + resource_group_name=rg, resource_name=hub_name + ) + policy_list = [] + + try: + while True: + policy_list.extend(policy_pager.advance_page()) + except StopIteration: + pass + + for policy in policy_list: + rights_set = set(policy.rights.split(", ")) + if PRIVILEDGED_ACCESS_RIGHTS_SET == rights_set: + logger.info("Using policy '%s' for IoT Hub interaction.", policy.key_name) + return policy + + raise CLIError( + "Unable to discover a priviledged policy for IoT Hub: {}, in subscription {}. " + "When interfacing with an IoT Hub, the IoT extension requires any single policy with " + "'RegistryWrite', 'ServiceConnect' and 'DeviceConnect' rights.".format( + hub_name, self.sub_id + ) + ) + + @classmethod + def get_target_by_cstring(cls, connection_string: str) -> IotHubTarget: + return IotHubTarget.from_connection_string(cstring=connection_string).as_dict() + + def get_target(self, hub_name, rg=None, **kwargs) -> Dict[str, str]: + cstring = kwargs.get("login") + if cstring: + return self.get_target_by_cstring(connection_string=cstring) + + target_iothub = self.find_iothub(hub_name=hub_name, rg=rg) + + policy_name = kwargs.get("policy_name", "auto") + rg = target_iothub.additional_properties.get("resourcegroup") + + target_policy = self.find_policy( + 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, + key_type=key_type, + include_events=include_events, + ) + + def _build_target( + self, iothub, policy, key_type=None, include_events=False + ) -> Dict[str, str]: + # This is more or less a compatibility function which produces the + # same result as _azure.get_iot_hub_connection_string() + # In future iteration we will return a 'Target' object rather than dict + # but that will be better served aligning with vNext pattern for Iot Hub + + target = {} + target["cs"] = CONN_STR_TEMPLATE.format( + iothub.properties.host_name, + policy.key_name, + policy.primary_key if key_type == "primary" else policy.secondary_key, + ) + target["entity"] = iothub.properties.host_name + target["policy"] = policy.key_name + target["primarykey"] = policy.primary_key + target["secondarykey"] = policy.secondary_key + target["subscription"] = self.sub_id + target["resourcegroup"] = iothub.additional_properties.get("resourcegroup") + target["location"] = iothub.location + target["sku_tier"] = iothub.sku.tier.value + + if include_events: + events = {} + events["endpoint"] = trim_from_start( + iothub.properties.event_hub_endpoints["events"].endpoint, "sb://" + ).strip("/") + events["partition_count"] = iothub.properties.event_hub_endpoints[ + "events" + ].partition_count + events["path"] = iothub.properties.event_hub_endpoints["events"].path + events["partition_ids"] = iothub.properties.event_hub_endpoints[ + "events" + ].partition_ids + target["events"] = events + + return target diff --git a/azext_iot/operations/digitaltwin.py b/azext_iot/operations/digitaltwin.py index c394a7315..1156237fb 100644 --- a/azext_iot/operations/digitaltwin.py +++ b/azext_iot/operations/digitaltwin.py @@ -9,7 +9,7 @@ from azext_iot.constants import PNP_ENDPOINT from azext_iot._factory import _bind_sdk from azext_iot.common.shared import SdkType, ModelSourceType -from azext_iot.common._azure import get_iot_hub_connection_string +from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot.common.utility import ( shell_safe_json_parse, read_file_content, @@ -219,9 +219,8 @@ def iot_digitaltwin_invoke_command( if target_json or isinstance(target_json, bool): command_payload = target_json - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login - ) + discovery = IotHubDiscovery(cmd) + target = discovery.get_target(hub_name=hub_name, resource_group_name=resource_group_name, login=login) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) try: result = service_sdk.invoke_interface_command( @@ -257,9 +256,8 @@ def iot_digitaltwin_property_update( if target_json: interface_payload = target_json - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login - ) + discovery = IotHubDiscovery(cmd) + target = discovery.get_target(hub_name=hub_name, resource_group_name=resource_group_name, login=login) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) try: result = service_sdk.update_interfaces(device_id, interfaces=interface_payload) @@ -307,9 +305,8 @@ def iot_digitaltwin_monitor_events( def _iot_digitaltwin_interface_show( cmd, device_id, interface, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login - ) + discovery = IotHubDiscovery(cmd) + target = discovery.get_target(hub_name=hub_name, resource_group_name=resource_group_name, login=login) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) try: device_interface = service_sdk.get_interface(device_id, interface) @@ -321,9 +318,8 @@ def _iot_digitaltwin_interface_show( def _iot_digitaltwin_interface_list( cmd, device_id, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login - ) + discovery = IotHubDiscovery(cmd) + target = discovery.get_target(hub_name=hub_name, resource_group_name=resource_group_name, login=login) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) try: device_interfaces = service_sdk.get_interfaces(device_id) @@ -365,9 +361,8 @@ def _pnp_interface_elements(cmd, interface, target_type, repo_endpoint, repo_id, def _device_interface_elements( cmd, device_id, interface, target_type, hub_name, resource_group_name, login ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login - ) + discovery = IotHubDiscovery(cmd) + target = discovery.get_target(hub_name=hub_name, resource_group_name=resource_group_name, login=login) service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) interface_elements = [] try: diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 3bf0a7dce..514f675b1 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -18,7 +18,7 @@ ) from azext_iot.common.sas_token_auth import SasTokenAuthentication from azext_iot.common.shared import DeviceAuthType, SdkType, ProtocolType, ConfigType -from azext_iot.common._azure import get_iot_hub_connection_string +from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot.common.utility import ( shell_safe_json_parse, read_file_content, @@ -43,10 +43,10 @@ def iot_query( cmd, query_command, hub_name=None, top=None, resource_group_name=None, login=None ): top = _process_top(top) - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, rg=resource_group_name, login=login ) - resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -65,8 +65,9 @@ def iot_query( def iot_device_show( cmd, device_id, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_device_show(target, device_id) @@ -123,8 +124,9 @@ def iot_device_create( login=None, ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -242,8 +244,9 @@ def _create_self_signed_cert(subject, valid_days, output_path=None): def iot_device_update( cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -289,8 +292,9 @@ def _handle_device_update_params(parameters): def iot_device_delete( cmd, device_id, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -316,8 +320,9 @@ def iot_device_delete( def iot_device_get_parent( cmd, device_id, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) child_device = _iot_device_show(target, device_id) _validate_nonedge_device(child_device) @@ -338,8 +343,9 @@ def iot_device_set_parent( resource_group_name=None, login=None, ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) parent_device = _iot_device_show(target, parent_id) _validate_edge_device(parent_device) @@ -358,8 +364,9 @@ def iot_device_children_add( resource_group_name=None, login=None, ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) devices = [] edge_device = _iot_device_show(target, device_id) @@ -385,8 +392,9 @@ def iot_device_children_remove( resource_group_name=None, login=None, ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) devices = [] if remove_all: @@ -442,8 +450,9 @@ def iot_device_children_list( def _iot_device_children_list( cmd, device_id, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) device = _iot_device_show(target, device_id) _validate_edge_device(device) @@ -541,8 +550,9 @@ def iot_device_module_create( cert = _create_self_signed_cert(module_id, valid_days, output_dir) primary_thumbprint = cert["thumbprint"] - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -579,8 +589,9 @@ def iot_device_module_update( resource_group_name=None, login=None, ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -637,8 +648,9 @@ def _parse_auth(parameters): def iot_device_module_list( cmd, device_id, hub_name=None, top=1000, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -652,8 +664,9 @@ def iot_device_module_list( def iot_device_module_show( cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_device_module_show(target, device_id, module_id) @@ -675,8 +688,9 @@ 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 ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -703,8 +717,9 @@ 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 ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_device_module_twin_show( target=target, device_id=device_id, module_id=module_id @@ -734,8 +749,9 @@ def iot_device_module_twin_update( ): from azext_iot.common.utility import verify_transform - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -771,8 +787,9 @@ def iot_device_module_twin_replace( resource_group_name=None, login=None, ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -804,8 +821,9 @@ def iot_edge_set_modules( ): from azext_iot.sdk.iothub.service.models import ConfigurationContent - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -898,8 +916,9 @@ def _iot_hub_configuration_create( ConfigurationMetrics, ) - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1045,8 +1064,9 @@ def iot_hub_configuration_update( from azext_iot.sdk.service.models.configuration import Configuration from azext_iot.common.utility import verify_transform - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1083,8 +1103,9 @@ def iot_hub_configuration_update( def iot_hub_configuration_show( cmd, config_id, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_hub_configuration_show(target=target, config_id=config_id) @@ -1137,8 +1158,9 @@ def _iot_hub_configuration_list( ): top = _process_top(top, upper_limit=100) - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1157,8 +1179,9 @@ def _iot_hub_configuration_list( def iot_hub_configuration_delete( cmd, config_id, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1207,8 +1230,9 @@ def iot_hub_configuration_metric_show( resource_group_name=None, login=None, ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1252,8 +1276,9 @@ def iot_hub_configuration_metric_show( def iot_device_twin_show( cmd, device_id, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_device_twin_show(target=target, device_id=device_id) @@ -1287,8 +1312,9 @@ def iot_device_twin_update( ): from azext_iot.common.utility import verify_transform - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1315,8 +1341,9 @@ def iot_device_twin_update( def iot_device_twin_replace( cmd, device_id, target_json, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1363,8 +1390,9 @@ def iot_device_method( "timeout must be at least {} seconds".format(METHOD_INVOKE_MIN_TIMEOUT_SEC) ) - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1414,8 +1442,9 @@ def iot_device_module_method( "timeout must not be over {} seconds".format(METHOD_INVOKE_MIN_TIMEOUT_SEC) ) - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) @@ -1500,8 +1529,12 @@ def _iot_build_sas_token( parse_iot_device_module_connection_string, ) - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, policy_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, + resource_group_name=resource_group_name, + policy_name=policy_name, + login=login, ) uri = None policy = None @@ -1636,8 +1669,9 @@ def iot_device_send_message( login=None, qos=1, ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_device_send_message( target=target, @@ -1702,8 +1736,9 @@ def iot_device_send_message_http( resource_group_name=None, login=None, ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_device_send_message_http(target, device_id, data, headers) @@ -1723,8 +1758,9 @@ def _iot_device_send_message_http(target, device_id, data, headers=None): def iot_c2d_message_complete( cmd, device_id, etag, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_c2d_message_complete(target, device_id, etag) @@ -1744,8 +1780,9 @@ def _iot_c2d_message_complete(target, device_id, etag): def iot_c2d_message_reject( cmd, device_id, etag, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_c2d_message_reject(target, device_id, etag) @@ -1765,8 +1802,9 @@ def _iot_c2d_message_reject(target, device_id, etag): def iot_c2d_message_abandon( cmd, device_id, etag, hub_name=None, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_c2d_message_abandon(target, device_id, etag) @@ -1786,8 +1824,9 @@ def _iot_c2d_message_abandon(target, device_id, etag): def iot_c2d_message_receive( cmd, device_id, hub_name=None, lock_timeout=60, resource_group_name=None, login=None ): - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_c2d_message_receive(target, device_id, lock_timeout) @@ -1880,8 +1919,9 @@ def iot_c2d_message_send( config = cmd.cli_ctx.config ensure_uamqp(config, yes, repair) - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) if properties: @@ -1957,8 +1997,9 @@ def iot_simulate_device( user_properties = validate_key_value_pairs(properties) or {} properties_to_send.update(user_properties) - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) token = None @@ -2053,7 +2094,10 @@ def iot_device_export( from azure.mgmt.iothub import __version__ as iot_sdk_version client = iot_hub_service_factory(cmd.cli_ctx) - target = get_iot_hub_connection_string(client, hub_name, resource_group_name) + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name + ) if ensure_min_version(iot_sdk_version, "0.12.0"): from azure.mgmt.iothub.models import ExportDevicesRequest @@ -2096,7 +2140,10 @@ def iot_device_import( from azure.mgmt.iothub import __version__ as iot_sdk_version client = iot_hub_service_factory(cmd.cli_ctx) - target = get_iot_hub_connection_string(client, hub_name, resource_group_name) + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name + ) if ensure_min_version(iot_sdk_version, "0.12.0"): from azure.mgmt.iothub.models import ImportDevicesRequest @@ -2140,8 +2187,9 @@ def iot_device_upload_file( ): from azext_iot.sdk.iothub.device.models import FileUploadCompletionStatus - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) resolver = SdkResolver(target=target, device_id=device_id) @@ -2238,8 +2286,9 @@ def iot_hub_monitor_feedback( config = cmd.cli_ctx.config ensure_uamqp(config, yes, repair) - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) return _iot_hub_monitor_feedback( @@ -2248,11 +2297,14 @@ def iot_hub_monitor_feedback( def iot_hub_distributed_tracing_show( - cmd, hub_name, device_id, resource_group_name=None + cmd, hub_name, device_id, resource_group_name=None, login=None, ): - device_twin = _iot_hub_distributed_tracing_show( - cmd, hub_name, device_id, resource_group_name + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, resource_group_name=resource_group_name, login=login ) + + device_twin = _iot_hub_distributed_tracing_show(target=target, device_id=device_id) return _customize_device_tracing_output( device_twin["deviceId"], device_twin["properties"]["desired"], @@ -2290,8 +2342,12 @@ def _iot_hub_monitor_events( for device_result in devices_result: device_ids[device_result["deviceId"]] = True - target = get_iot_hub_connection_string( - cmd, hub_name, resource_group_name, include_events=True, login=login + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, + resource_group_name=resource_group_name, + include_events=True, + login=login, ) from azext_iot.monitor.builders import hub_target_builder @@ -2332,15 +2388,27 @@ def _iot_hub_monitor_events( def iot_hub_distributed_tracing_update( - cmd, hub_name, device_id, sampling_mode, sampling_rate, resource_group_name=None + cmd, + hub_name, + device_id, + sampling_mode, + sampling_rate, + resource_group_name=None, + login=None, ): + discovery = IotHubDiscovery(cmd) + target = discovery.get_target( + hub_name=hub_name, + resource_group_name=resource_group_name, + include_events=True, + login=login, + ) + if int(sampling_rate) not in range(0, 101): raise CLIError( "Sampling rate is a percentage, So only values from 0 to 100(inclusive) are permitted." ) - device_twin = _iot_hub_distributed_tracing_show( - cmd, hub_name, device_id, resource_group_name - ) + device_twin = _iot_hub_distributed_tracing_show(target=target, device_id=device_id) if TRACING_PROPERTY not in device_twin["properties"]["desired"]: device_twin["properties"]["desired"][TRACING_PROPERTY] = {} device_twin["properties"]["desired"][TRACING_PROPERTY]["sampling_rate"] = int( @@ -2350,7 +2418,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 + cmd, device_id, device_twin, hub_name, resource_group_name, login ) return _customize_device_tracing_output( result.device_id, result.properties.desired, result.properties.reported @@ -2365,11 +2433,8 @@ def _iot_hub_monitor_feedback(target, device_id, wait_on_id): ) -def _iot_hub_distributed_tracing_show( - cmd, hub_name, device_id, resource_group_name=None -): - target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name) - device_twin = iot_device_twin_show(cmd, device_id, hub_name, resource_group_name) +def _iot_hub_distributed_tracing_show(target, device_id): + device_twin = _iot_device_twin_show(target=target, device_id=device_id) _validate_device_tracing(target, device_twin) return device_twin diff --git a/azext_iot/tests/conftest.py b/azext_iot/tests/conftest.py index 7dfb38ae0..aab443b5d 100644 --- a/azext_iot/tests/conftest.py +++ b/azext_iot/tests/conftest.py @@ -16,7 +16,7 @@ path_iot_hub_service_factory = "azext_iot.common._azure.iot_hub_service_factory" path_service_client = "msrest.service_client.ServiceClient.send" -path_ghcs = "azext_iot.operations.hub.get_iot_hub_connection_string" +path_ghcs = "azext_iot.iothub.providers.discovery.IotHubDiscovery.get_target" path_sas = "azext_iot._factory.SasTokenAuthentication" path_mqtt_client = "azext_iot.operations._mqtt.mqtt.Client" path_iot_hub_monitor_events_entrypoint = ( @@ -48,8 +48,14 @@ def generate_cs( return result.lower() if lower_case else result +# Sets current working directory to the directory of the executing file @pytest.fixture() -def fixture_cmd2(mocker): +def set_cwd(request): + os.chdir(os.path.dirname(os.path.abspath(str(request.fspath)))) + + +@pytest.fixture() +def fixture_cmd(mocker): cli = DummyCli() cli.loader = mocker.MagicMock() cli.loader.cli_ctx = cli @@ -60,20 +66,6 @@ def test_handler1(): return AzCliCommand(cli.loader, "iot-extension command", test_handler1) -# Sets current working directory to the directory of the executing file -@pytest.fixture() -def set_cwd(request): - os.chdir(os.path.dirname(os.path.abspath(str(request.fspath)))) - - -@pytest.fixture() -def fixture_cmd(mocker): - # Placeholder for later use - mocker.patch(path_iot_hub_service_factory) - cmd = mocker.MagicMock(name="cli cmd context") - return cmd - - @pytest.fixture() def fixture_service_client_generic(mocker, fixture_ghcs, fixture_sas): service_client = mocker.patch(path_service_client) diff --git a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py index 0264715cd..7f43a97c8 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py @@ -126,7 +126,7 @@ def service_client( ) def test_config_metric_show( self, - fixture_cmd2, + fixture_cmd, service_client, metric_id, content_type, @@ -141,7 +141,7 @@ def test_config_metric_show( else partial(subject.iot_hub_configuration_metric_show) ) result = target_method( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, metric_type=metric_type, metric_id=metric_id, @@ -174,7 +174,7 @@ def test_config_metric_show( ], ) def test_config_metric_show_invalid_args( - self, fixture_cmd2, service_client, metric_id, content_type, metric_type + self, fixture_cmd, service_client, metric_id, content_type, metric_type ): from functools import partial service_client.assert_all_requests_are_fired = False @@ -187,7 +187,7 @@ def test_config_metric_show_invalid_args( ) target_method( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, metric_type=metric_type, metric_id=metric_id, @@ -206,9 +206,9 @@ def serviceclient( ) return service_client - def test_config_show(self, serviceclient, fixture_cmd2): + def test_config_show(self, serviceclient, fixture_cmd): result = subject.iot_hub_configuration_show( - fixture_cmd2, config_id=config_id, hub_name=mock_target["entity"] + fixture_cmd, config_id=config_id, hub_name=mock_target["entity"] ) args = serviceclient.call_args @@ -219,10 +219,10 @@ def test_config_show(self, serviceclient, fixture_cmd2): assert method == "GET" assert isinstance(result, dict) - def test_config_show_error(self, serviceclient_generic_error, fixture_cmd2): + def test_config_show_error(self, serviceclient_generic_error, fixture_cmd): with pytest.raises(CLIError): subject.iot_hub_configuration_show( - fixture_cmd2, config_id=config_id, hub_name=mock_target["entity"] + fixture_cmd, config_id=config_id, hub_name=mock_target["entity"] ) @@ -255,7 +255,7 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): ) def test_config_create_edge( self, - fixture_cmd2, + fixture_cmd, serviceclient, sample_config_edge, sample_config_metrics, @@ -266,7 +266,7 @@ def test_config_create_edge( labels, ): subject.iot_edge_deployment_create( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=hub_name, content=sample_config_edge[1], @@ -329,7 +329,7 @@ def test_config_create_edge( ) def test_config_create_edge_malformed( self, - fixture_cmd2, + fixture_cmd, serviceclient, sample_config_edge_malformed, config_id, @@ -340,7 +340,7 @@ def test_config_create_edge_malformed( ): with pytest.raises(CLIError) as exc: subject.iot_edge_deployment_create( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=hub_name, content=sample_config_edge_malformed, @@ -378,7 +378,7 @@ def test_config_create_edge_malformed( ) def test_config_create_adm( self, - fixture_cmd2, + fixture_cmd, serviceclient, sample_config_adm, sample_config_metrics, @@ -400,7 +400,7 @@ def test_config_create_adm( target_condition = "FROM devices.modules WHERE {}".format(target_condition) subject.iot_hub_configuration_create( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=hub_name, content=sample_config_adm[1], @@ -470,7 +470,7 @@ def _assert_config_metrics_request(self, sample_config_metrics, body): ) def test_config_create_adm_invalid( self, - fixture_cmd2, + fixture_cmd, serviceclient, config_id, hub_name, @@ -480,7 +480,7 @@ def test_config_create_adm_invalid( ): with pytest.raises(CLIError) as exc1: subject.iot_hub_configuration_create( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=hub_name, content=get_context_path(__file__, "test_edge_deployment.json"), @@ -493,7 +493,7 @@ def test_config_create_adm_invalid( content = json.dumps({"deviceContent": {}, "moduleContent": {}}) with pytest.raises(CLIError) as exc2: subject.iot_hub_configuration_create( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=hub_name, content=content, @@ -512,7 +512,7 @@ def test_config_create_adm_invalid( content = json.dumps({"moduleContent": {"key": "value"}}) with pytest.raises(CLIError) as exc3: subject.iot_hub_configuration_create( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=hub_name, content=content, @@ -540,7 +540,7 @@ def test_config_create_adm_invalid( ) def test_config_create_error( self, - fixture_cmd2, + fixture_cmd, serviceclient_generic_error, sample_config_edge, config_id, @@ -551,7 +551,7 @@ def test_config_create_error( ): with pytest.raises(CLIError): subject.iot_edge_deployment_create( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=hub_name, content=sample_config_edge[1], @@ -574,9 +574,9 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client.side_effect = side_effect return service_client - def test_config_delete(self, serviceclient, fixture_cmd2): + def test_config_delete(self, serviceclient, fixture_cmd): subject.iot_hub_configuration_delete( - fixture_cmd2, config_id=config_id, hub_name=mock_target["entity"] + fixture_cmd, config_id=config_id, hub_name=mock_target["entity"] ) args = serviceclient.call_args url = args[0][0].url @@ -590,19 +590,19 @@ def test_config_delete(self, serviceclient, fixture_cmd2): @pytest.mark.parametrize("expected_error", [CLIError]) def test_config_delete_invalid_args( self, - fixture_cmd2, + fixture_cmd, serviceclient_generic_invalid_or_missing_etag, expected_error, ): with pytest.raises(expected_error): subject.iot_hub_configuration_delete( - fixture_cmd2, config_id=config_id, hub_name=mock_target["entity"] + fixture_cmd, config_id=config_id, hub_name=mock_target["entity"] ) - def test_config_delete_error(self, fixture_cmd2, serviceclient_generic_error): + def test_config_delete_error(self, fixture_cmd, serviceclient_generic_error): with pytest.raises(CLIError): subject.iot_hub_configuration_delete( - fixture_cmd2, config_id=config_id, hub_name=mock_target["entity"] + fixture_cmd, config_id=config_id, hub_name=mock_target["entity"] ) @@ -613,9 +613,9 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client.return_value = build_mock_response(mocker, request.param, {}) return service_client - def test_config_update(self, fixture_cmd2, serviceclient, sample_config_show): + def test_config_update(self, fixture_cmd, serviceclient, sample_config_show): subject.iot_hub_configuration_update( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=mock_target["entity"], parameters=sample_config_show, @@ -639,7 +639,7 @@ def test_config_update(self, fixture_cmd2, serviceclient, sample_config_show): assert body.get("labels") == sample_config_show.get("labels") def test_config_update_invalid_args( - self, fixture_cmd2, serviceclient, sample_config_show + self, fixture_cmd, serviceclient, sample_config_show ): from copy import deepcopy @@ -648,7 +648,7 @@ def test_config_update_invalid_args( with pytest.raises(CLIError): subject.iot_hub_configuration_update( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=mock_target["entity"], parameters=request, @@ -659,7 +659,7 @@ def test_config_update_invalid_args( with pytest.raises(CLIError) as exc_label: subject.iot_hub_configuration_update( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=mock_target["entity"], parameters=request, @@ -670,10 +670,10 @@ def test_config_update_invalid_args( "Input: not a dictionary. Review inline JSON examples here --> " "https://github.com/Azure/azure-iot-cli-extension/wiki/Tips".format(type_name)) - def test_config_update_error(self, fixture_cmd2, serviceclient_generic_error): + def test_config_update_error(self, fixture_cmd, serviceclient_generic_error): with pytest.raises(CLIError): subject.iot_hub_configuration_update( - cmd=fixture_cmd2, + cmd=fixture_cmd, config_id=config_id, hub_name=mock_target["entity"], parameters={}, @@ -715,9 +715,9 @@ def service_client(self, mocked_response, fixture_ghcs, request): yield mocked_response @pytest.mark.parametrize("top", [1, 100]) - def test_config_list(self, fixture_cmd2, service_client, top): + def test_config_list(self, fixture_cmd, service_client, top): result = subject.iot_hub_configuration_list( - cmd=fixture_cmd2, hub_name=mock_target["entity"], top=top + cmd=fixture_cmd, hub_name=mock_target["entity"], top=top ) assert json.dumps(result) @@ -728,9 +728,9 @@ def test_config_list(self, fixture_cmd2, service_client, top): assert "top={}".format(top) in list_request.url @pytest.mark.parametrize("top", [1, 10]) - def test_deployment_list(self, fixture_cmd2, service_client, top): + def test_deployment_list(self, fixture_cmd, service_client, top): result = subject.iot_edge_deployment_list( - cmd=fixture_cmd2, hub_name=mock_target["entity"], top=top + cmd=fixture_cmd, hub_name=mock_target["entity"], top=top ) assert json.dumps(result) @@ -740,17 +740,17 @@ def test_deployment_list(self, fixture_cmd2, service_client, top): assert "top={}".format(top) in list_request.url @pytest.mark.parametrize("top", [-1, 0, 101]) - def test_config_list_invalid_args(self, fixture_cmd2, top): + def test_config_list_invalid_args(self, fixture_cmd, top): with pytest.raises(CLIError): subject.iot_hub_configuration_list( - cmd=fixture_cmd2, hub_name=mock_target["entity"], top=top + cmd=fixture_cmd, hub_name=mock_target["entity"], top=top ) - def test_config_list_error(self, fixture_cmd2, service_client_generic_errors): + def test_config_list_error(self, fixture_cmd, service_client_generic_errors): service_client_generic_errors.assert_all_requests_are_fired = False with pytest.raises(CLIError): subject.iot_hub_configuration_list( - cmd=fixture_cmd2, hub_name=mock_target["entity"] + cmd=fixture_cmd, hub_name=mock_target["entity"] ) @@ -768,14 +768,14 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): "device_id, hub_name", [("test-device-01", mock_target["entity"])] ) def test_config_apply_edge( - self, fixture_cmd2, serviceclient, device_id, hub_name, sample_config_edge + self, fixture_cmd, serviceclient, device_id, hub_name, sample_config_edge ): # Not yet supporting layered deployments if sample_config_edge[0] == "layered": return result = subject.iot_edge_set_modules( - cmd=fixture_cmd2, + cmd=fixture_cmd, device_id=device_id, hub_name=mock_target["entity"], content=sample_config_edge[1], @@ -832,7 +832,7 @@ def test_config_apply_edge( ) def test_config_apply_edge_malformed( self, - fixture_cmd2, + fixture_cmd, serviceclient, device_id, hub_name, @@ -840,7 +840,7 @@ def test_config_apply_edge_malformed( ): with pytest.raises(CLIError) as exc: subject.iot_edge_set_modules( - cmd=fixture_cmd2, + cmd=fixture_cmd, device_id=device_id, hub_name=mock_target["entity"], content=sample_config_edge_malformed, @@ -858,7 +858,7 @@ def test_config_apply_edge_malformed( ) def test_config_apply_edge_error( self, - fixture_cmd2, + fixture_cmd, serviceclient_generic_error, device_id, hub_name, @@ -866,7 +866,7 @@ def test_config_apply_edge_error( ): with pytest.raises(CLIError): subject.iot_edge_set_modules( - cmd=fixture_cmd2, + cmd=fixture_cmd, device_id=device_id, hub_name=mock_target["entity"], content=sample_config_edge_malformed, diff --git a/azext_iot/tests/iothub/jobs/conftest.py b/azext_iot/tests/iothub/jobs/conftest.py deleted file mode 100644 index f7effeffe..000000000 --- a/azext_iot/tests/iothub/jobs/conftest.py +++ /dev/null @@ -1,19 +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. -# -------------------------------------------------------------------------------------------- - - -import pytest - -from ...conftest import mock_target - -path_ghcs = "azext_iot.iothub.providers.base.get_iot_hub_connection_string" - - -@pytest.fixture() -def fixture_ghcs(mocker): - ghcs = mocker.patch(path_ghcs) - ghcs.return_value = mock_target - return ghcs diff --git a/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py b/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py index 30a28110d..ccce786f6 100644 --- a/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py +++ b/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py @@ -214,7 +214,7 @@ def serviceclient_test_wait(self, mocker, fixture_ghcs, fixture_sas, request): ) def test_job_create( self, - fixture_cmd2, + fixture_cmd, serviceclient, job_id, job_type, @@ -229,7 +229,7 @@ def test_job_create( ): job_create = partial( subject.job_create, - cmd=fixture_cmd2, + cmd=fixture_cmd, job_id=job_id, job_type=job_type, hub_name=hub_name, @@ -304,7 +304,7 @@ def test_job_create( ) def test_job_create_wait( self, - fixture_cmd2, + fixture_cmd, serviceclient_test_wait, job_id, job_type, @@ -319,7 +319,7 @@ def test_job_create_wait( ): job_create = partial( subject.job_create, - cmd=fixture_cmd2, + cmd=fixture_cmd, job_id=job_id, job_type=job_type, hub_name=hub_name, @@ -453,7 +453,7 @@ def test_job_create_wait( ) def test_job_create_malformed( self, - fixture_cmd2, + fixture_cmd, serviceclient, job_id, job_type, @@ -472,7 +472,7 @@ def test_job_create_malformed( with pytest.raises(CLIError) as exc: job_create = partial( subject.job_create, - cmd=fixture_cmd2, + cmd=fixture_cmd, job_id=job_id, job_type=job_type, hub_name=hub_name, @@ -528,7 +528,7 @@ def test_job_create_malformed( ) def test_job_create_error( self, - fixture_cmd2, + fixture_cmd, serviceclient_generic_error, job_id, job_type, @@ -544,7 +544,7 @@ def test_job_create_error( with pytest.raises(CLIError): job_create = partial( subject.job_create, - cmd=fixture_cmd2, + cmd=fixture_cmd, job_id=job_id, job_type=job_type, hub_name=hub_name, @@ -601,10 +601,10 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): return service_client - def test_job_show(self, fixture_cmd2, serviceclient): + def test_job_show(self, fixture_cmd, serviceclient): target_job_id = generate_job_id() result = subject.job_show( - cmd=fixture_cmd2, job_id=target_job_id, hub_name=mock_target["entity"] + cmd=fixture_cmd, job_id=target_job_id, hub_name=mock_target["entity"] ) args_list = serviceclient.call_args_list @@ -630,12 +630,12 @@ def test_job_show(self, fixture_cmd2, serviceclient): assert result["startTime"] assert result["endTime"] - def test_job_show_error(self, fixture_cmd2, serviceclient_generic_error): + def test_job_show_error(self, fixture_cmd, serviceclient_generic_error): target_job_id = generate_job_id() with pytest.raises(CLIError): subject.job_show( - cmd=fixture_cmd2, job_id=target_job_id, hub_name=mock_target["entity"] + cmd=fixture_cmd, job_id=target_job_id, hub_name=mock_target["entity"] ) @@ -686,10 +686,10 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): return service_client - def test_job_cancel(self, fixture_cmd2, serviceclient): + def test_job_cancel(self, fixture_cmd, serviceclient): target_job_id = generate_job_id() result = subject.job_cancel( - cmd=fixture_cmd2, job_id=target_job_id, hub_name=mock_target["entity"] + cmd=fixture_cmd, job_id=target_job_id, hub_name=mock_target["entity"] ) args_list = serviceclient.call_args_list @@ -727,12 +727,12 @@ def test_job_cancel(self, fixture_cmd2, serviceclient): ) assert args_list[2][0][0].method == "DELETE" - def test_job_cancel_error(self, fixture_cmd2, serviceclient_generic_error): + def test_job_cancel_error(self, fixture_cmd, serviceclient_generic_error): target_job_id = generate_job_id() with pytest.raises(CLIError): subject.job_cancel( - cmd=fixture_cmd2, job_id=target_job_id, hub_name=mock_target["entity"] + cmd=fixture_cmd, job_id=target_job_id, hub_name=mock_target["entity"] ) @@ -870,9 +870,9 @@ def handle_calls(*args, **kwargs): (None, None, 5), ], ) - def test_job_list(self, fixture_cmd2, serviceclient, job_type, job_status, top): + def test_job_list(self, fixture_cmd, serviceclient, job_type, job_status, top): result = subject.job_list( - cmd=fixture_cmd2, + cmd=fixture_cmd, job_type=job_type, job_status=job_status, top=top, @@ -955,6 +955,6 @@ def _in_criteria(job_collection, job_status, job_type): return result - def test_job_list_error(self, fixture_cmd2, serviceclient_generic_error): + def test_job_list_error(self, fixture_cmd, serviceclient_generic_error): with pytest.raises(CLIError): - subject.job_list(cmd=fixture_cmd2, hub_name=mock_target["entity"]) + subject.job_list(cmd=fixture_cmd, hub_name=mock_target["entity"]) diff --git a/azext_iot/tests/test_iot_digitaltwin_unit.py b/azext_iot/tests/test_iot_digitaltwin_unit.py index 25aca9d62..e72878431 100644 --- a/azext_iot/tests/test_iot_digitaltwin_unit.py +++ b/azext_iot/tests/test_iot_digitaltwin_unit.py @@ -4,6 +4,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +# TODO: This module will look very different for the PnP refresh. + import pytest import json import os @@ -34,7 +36,7 @@ # Patch Paths # path_mqtt_client = "azext_iot.operations._mqtt.mqtt.Client" path_service_client = "msrest.service_client.ServiceClient.send" -path_ghcs = "azext_iot.operations.digitaltwin.get_iot_hub_connection_string" +path_ghcs = "azext_iot.iothub.providers.discovery.IotHubDiscovery.get_target" path_pnpcs = "azext_iot.operations.pnp.get_iot_pnp_connection_string" path_sas = "azext_iot._factory.SasTokenAuthentication" diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index 1bc9fe037..0c9138dc0 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -34,7 +34,6 @@ build_mock_response, path_service_client, mock_target, - hub_entity, generate_cs, ) @@ -502,7 +501,7 @@ def test_device_delete_error(self, serviceclient_generic_error): # Starting PoC for improving/simplyfing unit tests class TestDeviceShow: - def test_device_show(self, fixture_cmd2, mocked_response, fixture_ghcs): + def test_device_show(self, fixture_cmd, mocked_response, fixture_ghcs): device_id = generate_generic_id() mocked_response.add( method=responses.GET, @@ -513,12 +512,12 @@ def test_device_show(self, fixture_cmd2, mocked_response, fixture_ghcs): match_querystring=False ) - result = subject.iot_device_show(fixture_cmd2, device_id, mock_target["entity"]) + result = subject.iot_device_show(fixture_cmd, device_id, mock_target["entity"]) assert result["deviceId"] == device_id - def test_device_show_error(self, fixture_cmd2, service_client_generic_errors): + def test_device_show_error(self, fixture_cmd, service_client_generic_errors): with pytest.raises(CLIError): - subject.iot_device_show(fixture_cmd2, device_id, mock_target["entity"]) + subject.iot_device_show(fixture_cmd, device_id, mock_target["entity"]) class TestDeviceList: @@ -543,8 +542,8 @@ def service_client(self, mocked_response, fixture_ghcs, request): yield mocked_response @pytest.mark.parametrize("top, edge", [(10, True), (1000, False)]) - def test_device_list(self, fixture_cmd2, service_client, top, edge): - result = subject.iot_device_list(fixture_cmd2, mock_target["entity"], top, edge) + def test_device_list(self, fixture_cmd, service_client, top, edge): + result = subject.iot_device_list(fixture_cmd, mock_target["entity"], top, edge) list_request = service_client.calls[0].request body = json.loads(list_request.body) @@ -563,14 +562,14 @@ def test_device_list(self, fixture_cmd2, service_client, top, edge): assert headers["x-ms-max-item-count"] == str(top) @pytest.mark.parametrize("top", [-2, 0]) - def test_device_list_invalid_args(self, fixture_cmd2, top): + def test_device_list_invalid_args(self, fixture_cmd, top): with pytest.raises(CLIError): - subject.iot_device_list(fixture_cmd2, mock_target["entity"], top) + subject.iot_device_list(fixture_cmd, mock_target["entity"], top) - def test_device_list_error(self, fixture_cmd2, service_client_generic_errors): + def test_device_list_error(self, fixture_cmd, service_client_generic_errors): service_client_generic_errors.assert_all_requests_are_fired = False with pytest.raises(CLIError): - subject.iot_device_list(fixture_cmd2, mock_target["entity"]) + subject.iot_device_list(fixture_cmd, mock_target["entity"]) def generate_module_create_req( @@ -1438,163 +1437,6 @@ def test_device_method_error(self, serviceclient_generic_error): ) -hub_suffix = "awesome-azure.net" - - -# TODO: Refactor to leverage fixture pattern and reduce redundent params -class TestGetIoTHubConnString: - @pytest.mark.parametrize( - "hubcount, targethub, policy_name, rg_name, include_events, login, failure_reason, mgmt_sdk_ver", - [ - (5, "hub1", "iothubowner", str(uuid4()), False, None, None, "0.4"), - (0, "hub1", "iothubowner", None, False, True, None, "0.4"), - (5, "hub1", "iothubowner", str(uuid4()), False, None, None, None), - (0, "hub1", "iothubowner", None, False, True, None, None), - (1, "hub0", "iothubowner", None, True, None, None, None), - (10, "hub3", "custompolicy", "myrg", False, None, None, None), - (1, "hub0", "custompolicy", None, False, None, None, None), - (3, "hub4", "iothubowner", None, False, None, "subscription", None), - (1, "hub1", "iothubowner", "myrg", False, None, "policy", None), - (1, "myhub", "iothubowner", "myrg", False, None, "resource", None), - ], - ) - def test_get_hub_conn_string( - self, - mocker, - hubcount, - targethub, - policy_name, - rg_name, - include_events, - login, - failure_reason, - mgmt_sdk_ver, - ): - from azext_iot.common._azure import get_iot_hub_connection_string - - def _build_hub(hub, name, rg): - hub.name = name - hub.properties.host_name = "{}.{}".format(name, hub_suffix) - - if mgmt_sdk_ver == "0.4": - hub.resourcegroup = rg - hub.additional_properties = None - else: - d = {} - d["resourcegroup"] = rg - hub.additional_properties.return_value = d - hub.additional_properties.get.return_value = rg - - return hub - - def _build_policy(policy, name): - policy.key_name = name - policy.primary_key = mock_target["primarykey"] - policy.secondary_key = mock_target["secondarykey"] - return policy - - def _build_event(hub): - hub.properties.event_hub_endpoints = {"events": mocker.Mock()} - hub.properties.event_hub_endpoints["events"].endpoint = "sb://" + str( - uuid4() - ) - hub.properties.event_hub_endpoints["events"].partition_count = "2" - hub.properties.event_hub_endpoints["events"].path = hub_entity - hub.properties.event_hub_endpoints["events"].partition_ids = ["0", "1"] - return hub - - cmd = mocker.Mock(name="cmd") - ihsf = mocker.patch(path_iot_hub_service_factory) - client = mocker.Mock(name="hubclient") - - if not rg_name: - hub_list = [] - for i in range(0, hubcount): - hub_list.append( - _build_hub(mocker.Mock(), "hub{}".format(i), str(uuid4())) - ) - client.list_by_subscription.return_value = hub_list - else: - client.get.return_value = _build_hub(mocker.Mock(), targethub, rg_name) - - if failure_reason == "resource": - client.get.side_effect = ValueError - elif failure_reason == "policy": - client.get_keys_for_key_name.side_effect = ValueError - else: - client.get_keys_for_key_name.return_value = _build_policy( - mocker.Mock(), policy_name - ) - - if include_events: - _build_event(hub_list[0]) - - client.config.subscription_id = mock_target["subscription"] - ihsf.return_value = client - - if not failure_reason: - if not login: - result = get_iot_hub_connection_string( - cmd, targethub, rg_name, policy_name, include_events=include_events - ) - - expecting_hub = "{}.{}".format(targethub, hub_suffix) - assert result["entity"] == expecting_hub - assert result["policy"] == policy_name - assert result["subscription"] == mock_target["subscription"] - assert result["cs"] == generic_cs_template.format( - expecting_hub, policy_name, mock_target["primarykey"] - ) - - if rg_name: - client.get.assert_called_with(rg_name, targethub) - assert result["resourcegroup"] == rg_name - else: - assert result["resourcegroup"] - - client.get_keys_for_key_name.assert_called_with( - mocker.ANY, targethub, policy_name - ) - - if include_events: - assert result["events"]["endpoint"] - assert result["events"]["partition_count"] - assert result["events"]["path"] - assert result["events"]["partition_ids"] - else: - hub = str(uuid4()) - policy = str(uuid4()) - key = str(uuid4()) - cs = generate_cs(hub, policy, key) - lower_cs = generate_cs(hub, policy, key, lower_case=True) - - result = get_iot_hub_connection_string( - cmd, targethub, rg_name, policy_name, login=cs - ) - result_lower = get_iot_hub_connection_string( - cmd, targethub, rg_name, policy_name, login=lower_cs - ) - - assert result["entity"] == hub - assert result["policy"] == policy - assert result["primarykey"] == key - assert not result.get("resourcegroup") - assert not result.get("subscription") - assert result["cs"] == generic_cs_template.format(hub, policy, key) - assert result_lower["entity"] == hub - assert result_lower["policy"] == policy - assert result_lower["primarykey"] == key - assert not result_lower.get("resourcegroup") - assert not result_lower.get("subscription") - assert ( - result_lower["cs"] - == generic_cs_template.format(hub, policy, key).lower() - ) - else: - with pytest.raises(CLIError): - get_iot_hub_connection_string(client, targethub, rg_name, policy_name) - - class TestCloudToDeviceMessaging: @pytest.fixture(params=["full", "min"]) def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): From 37e8aa114dc0a387c8d65bf1d1f79fc9de56d84d Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 18 Jun 2020 13:11:21 -0700 Subject: [PATCH 054/179] Update HISTORY.rst --- HISTORY.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 44a7b5636..6eeb41af6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,13 @@ Release History =============== +0.9.5 ++++++++++++++++ +* IoT Hub commands now support dynamic privileged policy discovery. `iothubhowner` is no longer relied on. Instead any policy that has `RegistryWrite`, `ServiceConnect` and `DeviceConnect` permissions will be used. +* Monitoring commands (such as for `central` or `hub`) support module Id filter. Also it is more clear that an event comes from a module. +* Improved validation of central telemetry. +* Digital Twin endpoint create commands now support custom subscription options. + 0.9.4 +++++++++++++++ Azure Digital Twins Public Preview - CLI release From 0ffe1da7acccb2ccf6b5220a63852436f5d4cf59 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 19 Jun 2020 06:59:15 -0700 Subject: [PATCH 055/179] Create __init__.py --- azext_iot/monitor/central_validator/validators/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 azext_iot/monitor/central_validator/validators/__init__.py diff --git a/azext_iot/monitor/central_validator/validators/__init__.py b/azext_iot/monitor/central_validator/validators/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/monitor/central_validator/validators/__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. +# -------------------------------------------------------------------------------------------- From 957048562ebc834f769d3c03ebaaa12f50c8c771 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 19 Jun 2020 07:00:33 -0700 Subject: [PATCH 056/179] Update version --- azext_iot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azext_iot/constants.py b/azext_iot/constants.py index e6b48712c..f4e9035c1 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.5" +VERSION = "0.9.6" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" From 030e3b022742373abedbc9609812071c09911770 Mon Sep 17 00:00:00 2001 From: valluriraj Date: Mon, 29 Jun 2020 15:45:55 -0700 Subject: [PATCH 057/179] Added ability to monitor property updates for devices connected to IoT Central (#210) * add module level filtering - * Revert "add module level filtering -" This reverts commit 340a43c93e7575abaf22f493f3c548795ca02c74. * initial commit * additional fixes * additional changes * additional changes * Refactored * removed code * remove more code * propoerty monitoring updates * unit test updates * lint fixes * Revert "lint fixes" This reverts commit 00305d1f2fe6e817029d47d48a6ce706367658f1. * lint fixes * additional fixes * lint fixes and UT * fixed gettwin UT * Minor updates to comments etc Co-authored-by: Raj valluri Co-authored-by: prbans Co-authored-by: Paul Montgomery --- azext_iot/central/_help.py | 15 ++ azext_iot/central/command_map.py | 1 + azext_iot/central/commands_device_twin.py | 35 +--- azext_iot/central/commands_monitor.py | 13 ++ azext_iot/central/models/devicetwin.py | 34 ++++ .../central/providers/device_provider.py | 1 + .../central/providers/devicetwin_provider.py | 57 ++++++ azext_iot/common/utility.py | 4 + azext_iot/constants.py | 2 + azext_iot/monitor/property.py | 97 +++++++++ azext_iot/tests/central/json/device_twin.json | 186 ++++++++++++++++++ azext_iot/tests/test_constants.py | 1 + azext_iot/tests/test_iot_central_unit.py | 60 +++++- 13 files changed, 474 insertions(+), 32 deletions(-) create mode 100644 azext_iot/central/models/devicetwin.py create mode 100644 azext_iot/central/providers/devicetwin_provider.py create mode 100644 azext_iot/monitor/property.py create mode 100644 azext_iot/tests/central/json/device_twin.json diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index dd866c0c4..f09b624d1 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -316,6 +316,21 @@ def _load_central_monitors_help(): az iot central app validate-messages --app-id {app_id} -d {device_id} --cg {consumer_group_name} """ + helps[ + "iot central app monitor-properties" + ] = """ + type: command + short-summary: Monitor desired and reported properties sent to/from the IoT Hub for an IoT Central app. + long-summary: | + Polls device-twin from central and compares it to the last device-twin + Parses out properties from device-twin, and detects if changes were made + Prints subset of properties that were changed within the polling interval + examples: + - name: Basic usage + text: > + az iot central app monitor-properties --app-id {app_id} -d {device_id} + """ + # TODO: Delete this by July 2020 def _load_central_deprecated_commands(): diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index deab7ff06..f3f8baadb 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -36,6 +36,7 @@ def load_central_commands(self, _): ) as cmd_group: cmd_group.command("monitor-events", "monitor_events") cmd_group.command("validate-messages", "validate_messages", is_preview=True) + cmd_group.command("monitor-properties", "monitor_properties", is_preview=True) with self.command_group( "iot central app device", command_type=central_device_ops, is_preview=True, diff --git a/azext_iot/central/commands_device_twin.py b/azext_iot/central/commands_device_twin.py index 03d766d1d..f5ce9fcb4 100644 --- a/azext_iot/central/commands_device_twin.py +++ b/azext_iot/central/commands_device_twin.py @@ -4,36 +4,13 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from knack.util import CLIError -from azext_iot.constants import CENTRAL_ENDPOINT -from azext_iot._factory import _bind_sdk -from azext_iot.common.shared import SdkType -from azext_iot.common.utility import unpack_msrest_error -from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication - -def find_between(s, start, end): - return (s.split(start))[1].split(end)[0] +from azext_iot.constants import CENTRAL_ENDPOINT +from azext_iot.central.providers.devicetwin_provider import CentralDeviceTwinProvider def device_twin_show(cmd, device_id, app_id, central_dns_suffix=CENTRAL_ENDPOINT): - from azext_iot.common._azure import get_iot_central_tokens - - tokens = get_iot_central_tokens(cmd, app_id, central_dns_suffix) - exception = None - - # The device could be in any hub associated with the given app. - # We must search through each IoT Hub until device is found. - for token_group in tokens.values(): - sas_token = token_group["iothubTenantSasToken"]["sasToken"] - endpoint = find_between(sas_token, "SharedAccessSignature sr=", "&sig=") - target = {"entity": endpoint} - auth = BasicSasTokenAuthentication(sas_token=sas_token) - service_sdk, errors = _bind_sdk(target, SdkType.service_sdk, auth=auth) - try: - return service_sdk.get_twin(device_id) - except errors.CloudError as e: - if exception is None: - exception = CLIError(unpack_msrest_error(e)) - - raise exception + device_twin_provider = CentralDeviceTwinProvider( + cmd=cmd, app_id=app_id, device_id=device_id + ) + return device_twin_provider.get_device_twin(central_dns_suffix=central_dns_suffix) diff --git a/azext_iot/central/commands_monitor.py b/azext_iot/central/commands_monitor.py index 42aa504cf..fa49b0594 100644 --- a/azext_iot/central/commands_monitor.py +++ b/azext_iot/central/commands_monitor.py @@ -4,6 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- + from azure.cli.core.commands import AzCliCommand from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central.providers.monitor_provider import MonitorProvider @@ -14,6 +15,7 @@ CentralHandlerArguments, TelemetryArguments, ) +from azext_iot.monitor.property import start_property_monitor def validate_messages( @@ -112,3 +114,14 @@ def monitor_events( central_handler_args=central_handler_args, ) provider.start_monitor_events(telemetry_args) + + +def monitor_properties( + cmd, device_id, app_id, central_dns_suffix=CENTRAL_ENDPOINT, +): + start_property_monitor( + cmd=cmd, + device_id=device_id, + app_id=app_id, + central_dns_suffix=central_dns_suffix, + ) diff --git a/azext_iot/central/models/devicetwin.py b/azext_iot/central/models/devicetwin.py new file mode 100644 index 000000000..3ff74676a --- /dev/null +++ b/azext_iot/central/models/devicetwin.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +class DeviceTwin: + def __init__( + self, device_twin: dict, + ): + self.device_twin = device_twin + self.device_id = device_twin.get("deviceId") + self.desired_property = Property( + "desired property", + device_twin.get("properties", {}).get("desired"), + self.device_id, + ) + self.reported_property = Property( + "reported property", + device_twin.get("properties", {}).get("reported"), + self.device_id, + ) + + +class Property: + def __init__( + self, name: str, props: dict, device_id, + ): + self.name = name + self.props = props + self.metadata = props.get("$metadata") + self.version = props.get("$version") + self.device_id = device_id diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index 53ed84195..cc0f87f38 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -13,6 +13,7 @@ from azext_iot.central.models.device import Device from azext_iot.dps.services import global_service as dps_global_service + logger = get_logger(__name__) diff --git a/azext_iot/central/providers/devicetwin_provider.py b/azext_iot/central/providers/devicetwin_provider.py new file mode 100644 index 000000000..98c0b0b21 --- /dev/null +++ b/azext_iot/central/providers/devicetwin_provider.py @@ -0,0 +1,57 @@ +# 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 +from knack.log import get_logger +from azext_iot.common.utility import find_between +from azext_iot._factory import _bind_sdk +from azext_iot.common.shared import SdkType +from azext_iot.common.utility import unpack_msrest_error +from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication + + +logger = get_logger(__name__) + + +class CentralDeviceTwinProvider: + def __init__(self, cmd, app_id: str, device_id: str): + """ + Provider for devicetwin 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._device_id = device_id + + def get_device_twin(self, central_dns_suffix): + from azext_iot.common._azure import get_iot_central_tokens + + tokens = get_iot_central_tokens(self._cmd, self._app_id, central_dns_suffix) + + exception = None + + # The device could be in any hub associated with the given app. + # We must search through each IoT Hub until device is found. + for token_group in tokens.values(): + sas_token = token_group["iothubTenantSasToken"]["sasToken"] + endpoint = find_between(sas_token, "SharedAccessSignature sr=", "&sig=") + target = {"entity": endpoint} + auth = BasicSasTokenAuthentication(sas_token=sas_token) + service_sdk, errors = _bind_sdk(target, SdkType.service_sdk, auth=auth) + try: + return service_sdk.get_twin(self._device_id) + except errors.CloudError as e: + if exception is None: + exception = CLIError(unpack_msrest_error(e)) + + raise CLIError("Could not get device twin") diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index f93fbd7bb..1e8692987 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -478,3 +478,7 @@ def scantree(path): yield from scantree(entry.path) else: yield entry + + +def find_between(s, start, end): + return (s.split(start))[1].split(end)[0] diff --git a/azext_iot/constants.py b/azext_iot/constants.py index f4e9035c1..496ed201d 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -44,6 +44,8 @@ TRACING_ALLOWED_FOR_SKU = "standard" USER_AGENT = "IoTPlatformCliExtension/{}".format(VERSION) DIGITALTWINS_RESOURCE_ID = "https://digitaltwins.azure.net" +DEVICETWIN_POLLING_INTERVAL_SEC = 10 +DEVICETWIN_MONITOR_TIME_SEC = 15 # (Lib name, minimum version (including), maximum version (excluding)) EVENT_LIB = ("uamqp", "1.2", "1.3") diff --git a/azext_iot/monitor/property.py b/azext_iot/monitor/property.py new file mode 100644 index 000000000..adcbf0b13 --- /dev/null +++ b/azext_iot/monitor/property.py @@ -0,0 +1,97 @@ +# 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 datetime +import isodate +import time + +from azext_iot.constants import ( + CENTRAL_ENDPOINT, + DEVICETWIN_POLLING_INTERVAL_SEC, + DEVICETWIN_MONITOR_TIME_SEC, +) +from azext_iot.central.providers.devicetwin_provider import CentralDeviceTwinProvider +from azext_iot.central.models.devicetwin import DeviceTwin, Property + + +def start_property_monitor( + cmd, + device_id, + app_id, + central_dns_suffix=CENTRAL_ENDPOINT, + polling_interval_seconds=DEVICETWIN_POLLING_INTERVAL_SEC, +): + prev_twin = None + + device_twin_provider = CentralDeviceTwinProvider( + cmd=cmd, app_id=app_id, device_id=device_id + ) + + while True: + + raw_twin = device_twin_provider.get_device_twin( + central_dns_suffix=central_dns_suffix + ) + + twin = DeviceTwin(raw_twin) + if prev_twin: + change_d = compare_properties( + prev_twin.desired_property, twin.desired_property + ) + change_r = compare_properties( + prev_twin.reported_property, twin.reported_property + ) + + if change_d: + print("Changes in desired properties:") + print("version :", twin.desired_property.version) + print(change_d) + + if change_r: + print("Changes in reported properties:") + print("version :", twin.reported_property.version) + print(change_r) + + time.sleep(polling_interval_seconds) + + prev_twin = twin + + +def compare_properties(prev_prop: Property, prop: Property): + if prev_prop.version == prop.version: + return + + changes = { + key: changed_props(prop.props[key], prop.metadata[key], key) + for key, val in prop.metadata.items() + if is_relevant(key, val) + } + + return changes + + +def is_relevant(key, val): + if key in {"$lastUpdated", "$lastUpdatedVersion"}: + return False + + updated_within = datetime.datetime.now() - datetime.timedelta( + seconds=DEVICETWIN_MONITOR_TIME_SEC + ) + + last_updated = isodate.parse_datetime(val["$lastUpdated"]) + return last_updated.timestamp() >= updated_within.timestamp() + + +def changed_props(prop, metadata, property_name): + # not an interface - whole thing is change log + if "$iotin" not in property_name: + return prop + + # iterate over property in the interface + # if the property has been updated within DEVICETWIN_MONITOR_TIME_SEC + # track it as a change + diff = {key: prop[key] for key, val in metadata.items() if is_relevant(key, val)} + return diff diff --git a/azext_iot/tests/central/json/device_twin.json b/azext_iot/tests/central/json/device_twin.json new file mode 100644 index 000000000..1a112aff9 --- /dev/null +++ b/azext_iot/tests/central/json/device_twin.json @@ -0,0 +1,186 @@ +{ + "deviceId": "testDpsSetup", + "etag": "AAAAAAAAAIA=", + "deviceEtag": "NTMwMzM3ODQ4", + "status": "enabled", + "statusUpdateTime": "0001-01-01T00: 00: 00Z", + "connectionState": "Connected", + "lastActivityTime": "2020-06-25T06: 36: 07.5744727Z", + "cloudToDeviceMessageCount": 0, + "authenticationType": "sas", + "x509Thumbprint": { + "primaryThumbprint": "None", + "secondaryThumbprint": "None" + }, + "version": 2856, + "properties": { + "desired": { + "$iotin:settings": { + "fanSpeed": { + "value": 120 + } + }, + "$metadata": { + "$lastUpdated": "current_time", + "$lastUpdatedVersion": 128, + "$iotin:settings": { + "$lastUpdated": "current_time", + "$lastUpdatedVersion": 128, + "fanSpeed": { + "$lastUpdated": "current_time", + "$lastUpdatedVersion": 128, + "value": { + "$lastUpdated": "current_time", + "$lastUpdatedVersion": 128 + } + } + } + }, + "$version": 128 + }, + "reported": { + "$iotin:urn_azureiot_Client_SDKInformation": { + "language": { + "value": "C" + }, + "version": { + "value": "0.9.0" + }, + "vendor": { + "value": "Microsoft" + } + }, + "$iotin:deviceinfo": { + "manufacturer": { + "value": "MXChip" + }, + "model": { + "value": "AZ3166" + }, + "osName": { + "value": "Arm Mbed OS v5.2" + }, + "processorArchitecture": { + "value": "Arm Cortex M4" + }, + "swVersion": { + "value": "1.0.0" + }, + "processorManufacturer": { + "value": "STMicro" + }, + "totalStorage": { + "value": 2048 + }, + "totalMemory": { + "value": 256 + } + }, + "$iotin:settings": { + "fanSpeed": { + "value": 120, + "sc": 200, + "sd": "fanSpeed property is updated successfully", + "sv": 128 + } + }, + "$metadata": { + "$lastUpdated": "current_time", + "$iotin:urn_azureiot_Client_SDKInformation": { + "$lastUpdated": "current_time", + "language": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + }, + "version": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + }, + "vendor": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + } + }, + "$iotin:deviceinfo": { + "$lastUpdated": "current_time", + "manufacturer": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + }, + "model": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + }, + "osName": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + }, + "processorArchitecture": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + }, + "swVersion": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + }, + "processorManufacturer": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + }, + "totalStorage": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + }, + "totalMemory": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + } + } + }, + "$iotin:settings": { + "$lastUpdated": "current_time", + "fanSpeed": { + "$lastUpdated": "current_time", + "value": { + "$lastUpdated": "current_time" + }, + "sc": { + "$lastUpdated": "current_time" + }, + "sd": { + "$lastUpdated": "current_time" + }, + "sv": { + "$lastUpdated": "current_time" + } + } + } + }, + "$version": 2728 + } + }, + "capabilities": { + "iotEdge": "False" + } +} \ No newline at end of file diff --git a/azext_iot/tests/test_constants.py b/azext_iot/tests/test_constants.py index 242823530..c5b5fef61 100644 --- a/azext_iot/tests/test_constants.py +++ b/azext_iot/tests/test_constants.py @@ -11,3 +11,4 @@ class FileNames: "central/json/deeply_nested_template.json" ) central_device_file = "central/json/device.json" + central_device_twin_file = "central/json/device_twin.json" diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index 248eda325..0617815b0 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -6,7 +6,9 @@ import mock import pytest - +import json +import ast +from datetime import datetime from knack.util import CLIError from azure.cli.core.mock import DummyCli from azext_iot.central import commands_device_twin @@ -16,7 +18,8 @@ CentralDeviceProvider, CentralDeviceTemplateProvider, ) - +from azext_iot.central.models.devicetwin import DeviceTwin +from azext_iot.monitor.property import compare_properties from .helpers import load_json from .test_constants import FileNames @@ -41,7 +44,7 @@ class mock_service_sdk: def get_twin(self, device_id): return device_twin_result - mock = mocker.patch("azext_iot.central.commands_device_twin._bind_sdk") + mock = mocker.patch("azext_iot.central.providers.devicetwin_provider._bind_sdk") mock.return_value = (mock_service_sdk(), None) return mock @@ -154,6 +157,7 @@ def test_monitor_events_invalid_args(self, timeout, exception, fixture_cmd): class TestCentralDeviceProvider: _device = load_json(FileNames.central_device_file) _device_template = load_json(FileNames.central_device_template_file) + _device_twin = load_json(FileNames.central_device_twin_file) @mock.patch("azext_iot.central.services.device_template") @mock.patch("azext_iot.central.services.device") @@ -197,3 +201,53 @@ def test_should_return_device_template( # call counts should be at most 1 since the provider has a cache assert mock_device_template_svc.get_device_template.call_count == 1 assert template == self._device_template + + @mock.patch("azext_iot.central.services.device_template") + @mock.patch("azext_iot.central.services.device") + def test_should_return_updated_properties( + self, mock_device_svc, mock_device_template_svc + ): + # setup + device_twin_data = json.dumps(self._device_twin) + raw_twin = ast.literal_eval( + device_twin_data.replace("current_time", datetime.now().isoformat()) + ) + + twin = DeviceTwin(raw_twin) + twin_next = DeviceTwin(raw_twin) + twin_next.reported_property.version = twin.reported_property.version + 1 + result = compare_properties(twin_next.reported_property, twin.reported_property) + assert len(result) == 3 + assert len(result["$iotin:urn_azureiot_Client_SDKInformation"]) == 3 + assert result["$iotin:urn_azureiot_Client_SDKInformation"]["language"] + assert result["$iotin:urn_azureiot_Client_SDKInformation"]["version"] + assert result["$iotin:urn_azureiot_Client_SDKInformation"]["vendor"] + + assert len(result["$iotin:deviceinfo"]) == 8 + assert result["$iotin:deviceinfo"]["manufacturer"] + assert result["$iotin:deviceinfo"]["model"] + assert result["$iotin:deviceinfo"]["osName"] + assert result["$iotin:deviceinfo"]["processorArchitecture"] + assert result["$iotin:deviceinfo"]["swVersion"] + assert result["$iotin:deviceinfo"]["processorManufacturer"] + assert result["$iotin:deviceinfo"]["totalStorage"] + assert result["$iotin:deviceinfo"]["totalMemory"] + + assert len(result["$iotin:settings"]) == 1 + assert result["$iotin:settings"]["fanSpeed"] + + @mock.patch("azext_iot.central.services.device_template") + @mock.patch("azext_iot.central.services.device") + def test_should_return_no_properties( + self, mock_device_svc, mock_device_template_svc + ): + # test to check that no property updates are reported when version is not upadted + # setup + device_twin_data = json.dumps(self._device_twin) + raw_twin = ast.literal_eval( + device_twin_data.replace("current_time", datetime.now().isoformat()) + ) + twin = DeviceTwin(raw_twin) + twin_next = DeviceTwin(raw_twin) + result = compare_properties(twin_next.reported_property, twin.reported_property) + assert result is None From 3e6412ab281b8c9ae1be1cb0d73d48b778ed8fba Mon Sep 17 00:00:00 2001 From: Sapan Saxena <31940305+anusapan@users.noreply.github.com> Date: Tue, 30 Jun 2020 22:06:37 -0700 Subject: [PATCH 058/179] Add new allocation-policy to enrollment and enrollment-group (#203) * Add new allocation-policy to enrollment and group * Resolved PR comments --- azext_iot/_help.py | 10 ++++ azext_iot/_params.py | 11 ++++ azext_iot/common/shared.py | 1 + azext_iot/operations/dps.py | 65 ++++++++++++++++---- azext_iot/tests/test_iot_dps_int.py | 35 +++++++++-- azext_iot/tests/test_iot_dps_unit.py | 89 +++++++++++++++++++++++----- 6 files changed, 180 insertions(+), 31 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index b688997ae..b2d395cfb 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -945,6 +945,11 @@ az iot dps enrollment create -g {resource_group_name} --dps-name {dps_name} --enrollment-id {enrollment_id} --attestation-type tpm --allocation-policy hashed --endorsement-key 14963E8F3BA5B3984110B3C1CA8E8B89 --iot-hubs "{iot_hub_host_name1} {iot_hub_host_name2}" + - name: Create an enrollment 'MyEnrollment' with custom allocation policy, + text: > + az iot dps enrollment create -g {resource_group_name} --dps-name {dps_name} + --enrollment-id {enrollment_id} --attestation-type symmetrickey --allocation-policy custom + --webhook-url {webhook_url} --api-version {api_version} """ helps[ @@ -1044,6 +1049,11 @@ text: > az iot dps enrollment-group create -g {resource_group_name} --dps-name {dps_name} --enrollment-id {enrollment_id} --primary-key {primary_key} --secondary-key {secondary_key} + - name: Create an enrollment group '{enrollment_id}' with custom allocation policy, + text: > + az iot dps enrollment-group create -g {resource_group_name} --dps-name {dps_name} + --enrollment-id {enrollment_id} --allocation-policy custom --webhook-url {webhook_url} + --api-version {api_version} """ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 25f967b49..7133cc3f2 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -762,6 +762,17 @@ def load_arguments(self, _): options_list=["--iot-hubs", "--ih"], help="Host name of target IoT Hub. Use space-separated list for multiple IoT Hubs.", ) + context.argument( + "webhook_url", + options_list=["--webhook-url", "--wh"], + help="The webhook URL used for custom allocation requests.", + ) + context.argument( + "api_version", + options_list=["--api-version", "--av"], + help="The API version of the provisioning service types sent in the custom allocation" + " request. Minimum supported version: 2018-09-01-preview." + ) with self.argument_context("iot dps enrollment") as context: context.argument("enrollment_id", help="ID of device enrollment record") diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index 03d683633..b2659f4e9 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -131,6 +131,7 @@ class AllocationType(Enum): hashed = "hashed" geolatency = "geolatency" static = "static" + custom = "custom" class DistributedTracingSamplingModeType(Enum): diff --git a/azext_iot/operations/dps.py b/azext_iot/operations/dps.py index dd98994f4..d93cc7b70 100644 --- a/azext_iot/operations/dps.py +++ b/azext_iot/operations/dps.py @@ -16,6 +16,7 @@ from azext_iot.operations.generic import _execute_query from azext_iot._factory import _bind_sdk from azext_iot.sdk.dps.models.individual_enrollment import IndividualEnrollment +from azext_iot.sdk.dps.models.custom_allocation_definition import CustomAllocationDefinition from azext_iot.sdk.dps.models.attestation_mechanism import AttestationMechanism from azext_iot.sdk.dps.models.tpm_attestation import TpmAttestation from azext_iot.sdk.dps.models.symmetric_key_attestation import SymmetricKeyAttestation @@ -75,7 +76,9 @@ def iot_dps_device_enrollment_create(client, reprovision_policy=None, allocation_policy=None, iot_hubs=None, - edge_enabled=False): + edge_enabled=False, + webhook_url=None, + api_version=None): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) @@ -93,11 +96,17 @@ def iot_dps_device_enrollment_create(client, reprovision = _get_reprovision_policy(reprovision_policy) initial_twin = _get_initial_twin(initial_twin_tags, initial_twin_properties) iot_hub_list = iot_hubs.split() if iot_hubs else iot_hubs - _validate_allocation_policy_for_enrollment(allocation_policy, iot_hub_host_name, iot_hub_list) + _validate_allocation_policy_for_enrollment(allocation_policy, + iot_hub_host_name, + iot_hub_list, + webhook_url, + api_version) if iot_hub_host_name and allocation_policy is None: allocation_policy = AllocationType.static.value iot_hub_list = iot_hub_host_name.split() + custom_allocation_definition = CustomAllocationDefinition( + webhook_url, api_version) if allocation_policy == AllocationType.custom.value else None capabilities = DeviceCapabilities(iot_edge=edge_enabled) enrollment = IndividualEnrollment(enrollment_id, attestation, @@ -109,7 +118,8 @@ def iot_dps_device_enrollment_create(client, provisioning_status, reprovision, allocation_policy, - iot_hub_list) + iot_hub_list, + custom_allocation_definition) return m_sdk.device_enrollment.create_or_update(enrollment_id, enrollment) except errors.ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -135,7 +145,9 @@ def iot_dps_device_enrollment_update(client, reprovision_policy=None, allocation_policy=None, iot_hubs=None, - edge_enabled=None): + edge_enabled=None, + webhook_url=None, + api_version=None): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) @@ -186,12 +198,17 @@ def iot_dps_device_enrollment_update(client, initial_twin_tags, initial_twin_properties) iot_hub_list = iot_hubs.split() if iot_hubs else iot_hubs - _validate_allocation_policy_for_enrollment(allocation_policy, iot_hub_host_name, iot_hub_list) + _validate_allocation_policy_for_enrollment(allocation_policy, + iot_hub_host_name, + iot_hub_list, + webhook_url, + api_version) if allocation_policy: enrollment_record.allocation_policy = allocation_policy enrollment_record.iot_hubs = iot_hub_list enrollment_record.iot_hub_host_name = None - + if allocation_policy == AllocationType.custom.value: + enrollment_record.custom_allocation_definition = CustomAllocationDefinition(webhook_url, api_version) if edge_enabled is not None: enrollment_record.capabilities = DeviceCapabilities(iot_edge=edge_enabled) @@ -250,7 +267,9 @@ def iot_dps_device_enrollment_group_create(client, reprovision_policy=None, allocation_policy=None, iot_hubs=None, - edge_enabled=False): + edge_enabled=False, + webhook_url=None, + api_version=None): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) @@ -271,11 +290,18 @@ def iot_dps_device_enrollment_group_create(client, reprovision = _get_reprovision_policy(reprovision_policy) initial_twin = _get_initial_twin(initial_twin_tags, initial_twin_properties) iot_hub_list = iot_hubs.split() if iot_hubs else iot_hubs - _validate_allocation_policy_for_enrollment(allocation_policy, iot_hub_host_name, iot_hub_list) + _validate_allocation_policy_for_enrollment(allocation_policy, + iot_hub_host_name, + iot_hub_list, + webhook_url, + api_version) if iot_hub_host_name and allocation_policy is None: allocation_policy = AllocationType.static.value iot_hub_list = iot_hub_host_name.split() + custom_allocation_definition = CustomAllocationDefinition( + webhook_url, api_version) if allocation_policy == AllocationType.custom.value else None + capabilities = DeviceCapabilities(iot_edge=edge_enabled) group_enrollment = EnrollmentGroup(enrollment_id, attestation, @@ -286,7 +312,8 @@ def iot_dps_device_enrollment_group_create(client, provisioning_status, reprovision, allocation_policy, - iot_hub_list) + iot_hub_list, + custom_allocation_definition) return m_sdk.device_enrollment_group.create_or_update(enrollment_id, group_enrollment) except errors.ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -312,7 +339,9 @@ def iot_dps_device_enrollment_group_update(client, reprovision_policy=None, allocation_policy=None, iot_hubs=None, - edge_enabled=None): + edge_enabled=None, + webhook_url=None, + api_version=None): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) @@ -371,11 +400,17 @@ def iot_dps_device_enrollment_group_update(client, initial_twin_tags, initial_twin_properties) iot_hub_list = iot_hubs.split() if iot_hubs else iot_hubs - _validate_allocation_policy_for_enrollment(allocation_policy, iot_hub_host_name, iot_hub_list) + _validate_allocation_policy_for_enrollment(allocation_policy, + iot_hub_host_name, + iot_hub_list, + webhook_url, + api_version) if allocation_policy: enrollment_record.allocation_policy = allocation_policy enrollment_record.iot_hubs = iot_hub_list enrollment_record.iot_hub_host_name = None + if allocation_policy == AllocationType.custom.value: + enrollment_record.custom_allocation_definition = CustomAllocationDefinition(webhook_url, api_version) if edge_enabled is not None: enrollment_record.capabilities = DeviceCapabilities(iot_edge=edge_enabled) return m_sdk.device_enrollment_group.create_or_update(enrollment_id, enrollment_record, etag) @@ -610,7 +645,9 @@ def _validate_arguments_for_attestation_mechanism(attestation_type, def _validate_allocation_policy_for_enrollment(allocation_policy, iot_hub_host_name, - iot_hub_list): + iot_hub_list, + webhook_url, + api_version): if allocation_policy: if iot_hub_host_name is not None: raise CLIError('\'iot_hub_host_name\' is not required when allocation-policy is defined.') @@ -621,6 +658,10 @@ def _validate_allocation_policy_for_enrollment(allocation_policy, raise CLIError('Please provide a hub to be assigned with device.') if iot_hub_list and len(iot_hub_list) > 1: raise CLIError('Only one hub is required in static allocation policy.') + if allocation_policy == AllocationType.custom.value: + if webhook_url is None or api_version is None: + raise CLIError('Please provide both the Azure function webhook url and provisioning' + ' service api-version when the allocation-policy is defined as Custom.') else: if iot_hub_list: raise CLIError('Please provide allocation policy.') diff --git a/azext_iot/tests/test_iot_dps_int.py b/azext_iot/tests/test_iot_dps_int.py index a5c16a02d..3ea92dccd 100644 --- a/azext_iot/tests/test_iot_dps_int.py +++ b/azext_iot/tests/test_iot_dps_int.py @@ -163,12 +163,15 @@ def test_dps_enrollment_x509_lifecycle(self): def test_dps_enrollment_symmetrickey_lifecycle(self): enrollment_id = self.create_random_name('enrollment-for-test', length=48) + enrollment_id2 = self.create_random_name('enrollment-for-test', length=48) attestation_type = AttestationType.symmetricKey.value primary_key = 'x3XNu1HeSw93rmtDXduRUZjhqdGbcqR/zloWYiyPUzw=' secondary_key = 'PahMnOSBblv9CRn5B765iK35jTvnjDUjYP9hKBZa4Ug=' device_id = self.create_random_name('device-id-for-test', length=48) reprovisionPolicy_reprovisionandresetdata = 'reprovisionandresetdata' hub_host_name = '{}.azure-devices.net'.format(hub) + webhook_url = 'https://www.test.test' + api_version = '2019-03-31' etag = self.cmd('iot dps enrollment create --enrollment-id {} --attestation-type {}' ' -g {} --dps-name {} --pk {} --sk {}' @@ -210,14 +213,18 @@ def test_dps_enrollment_symmetrickey_lifecycle(self): self.cmd('iot dps enrollment update -g {} --dps-name {} --enrollment-id {}' ' --provisioning-status {} --etag {} --edge-enabled False' - .format(rg, dps, enrollment_id, self.provisioning_status_new, etag), + ' --allocation-policy {} --webhook-url {} --api-version {}' + .format(rg, dps, enrollment_id, self.provisioning_status_new, etag, + AllocationType.custom.value, webhook_url, api_version), checks=[ self.check('attestation.type', attestation_type), self.check('registrationId', enrollment_id), self.check('provisioningStatus', self.provisioning_status_new), self.check('deviceId', device_id), - self.check('allocationPolicy', "geoLatency"), + self.check('allocationPolicy', "custom"), + self.check('customAllocationDefinition.webhookUrl', webhook_url), + self.check('customAllocationDefinition.apiVersion', api_version), self.check('iotHubs', hub_host_name.split()), self.exists('initialTwin.tags'), self.exists('initialTwin.properties.desired'), @@ -225,13 +232,29 @@ def test_dps_enrollment_symmetrickey_lifecycle(self): self.check('capabilities.iotEdge', False) ]) + self.cmd('iot dps enrollment create --enrollment-id {} --attestation-type {}' + ' -g {} --dps-name {} --allocation-policy {} --webhook-url {} --api-version {}' + .format(enrollment_id2, attestation_type, rg, dps, AllocationType.custom.value, + webhook_url, api_version), + checks=[ + self.check('attestation.type', attestation_type), + self.check('registrationId', enrollment_id2), + self.check('allocationPolicy', 'custom'), + self.check('customAllocationDefinition.webhookUrl', webhook_url), + self.check('customAllocationDefinition.apiVersion', api_version), + ]) + self.cmd('iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}' .format(rg, dps, enrollment_id)) + self.cmd('iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}' + .format(rg, dps, enrollment_id2)) def test_dps_enrollment_group_lifecycle(self): enrollment_id = self.create_random_name('enrollment-for-test', length=48) reprovisionPolicy_never = 'never' hub_host_name = '{}.azure-devices.net'.format(hub) + webhook_url = 'https://www.test.test' + api_version = '2019-03-31' etag = self.cmd('iot dps enrollment-group create --enrollment-id {} -g {} --dps-name {}' ' --cp {} --scp {} --provisioning-status {} --allocation-policy {}' ' --iot-hubs {} --edge-enabled' @@ -287,12 +310,16 @@ def test_dps_enrollment_group_lifecycle(self): checks=[self.check('name', cert_name)]).get_output_in_json()['etag'] self.cmd('iot dps enrollment-group update -g {} --dps-name {} --enrollment-id {}' - ' --cn {} --etag {}' - .format(rg, dps, enrollment_id, cert_name, etag), + ' --cn {} --etag {} --allocation-policy {} --webhook-url {} --api-version {}' + .format(rg, dps, enrollment_id, cert_name, etag, + AllocationType.custom.value, webhook_url, api_version), checks=[ self.check('attestation.type', AttestationType.x509.value), self.check('enrollmentGroupId', enrollment_id), + self.check('allocationPolicy', "custom"), + self.check('customAllocationDefinition.webhookUrl', webhook_url), + self.check('customAllocationDefinition.apiVersion', api_version), self.check( 'attestation.x509.caReferences.primary', cert_name), self.check( diff --git a/azext_iot/tests/test_iot_dps_unit.py b/azext_iot/tests/test_iot_dps_unit.py index ab378eaec..8476df211 100644 --- a/azext_iot/tests/test_iot_dps_unit.py +++ b/azext_iot/tests/test_iot_dps_unit.py @@ -66,7 +66,7 @@ def generate_enrollment_create_req(attestation_type=None, endorsement_key=None, initial_twin_tags=None, initial_twin_properties=None, provisioning_status=None, reprovision_policy=None, primary_key=None, secondary_key=None, allocation_policy=None, - iot_hubs=None, edge_enabled=False): + iot_hubs=None, edge_enabled=False, webhook_url=None, api_version=None): return {'client': None, 'enrollment_id': enrollment_id, 'rg': resource_group, @@ -85,7 +85,9 @@ def generate_enrollment_create_req(attestation_type=None, endorsement_key=None, 'secondary_key': secondary_key, 'allocation_policy': allocation_policy, 'iot_hubs': iot_hubs, - 'edge_enabled': edge_enabled} + 'edge_enabled': edge_enabled, + 'webhook_url': webhook_url, + 'api_version': api_version} class TestEnrollmentCreate(): @@ -151,6 +153,13 @@ def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): secondary_key='secondarykey', reprovision_policy='never', allocation_policy='geolatency')), + (generate_enrollment_create_req(attestation_type='symmetricKey', + primary_key='primarykey', + secondary_key='secondarykey', + reprovision_policy='never', + allocation_policy='custom', + webhook_url="https://www.test.test", + api_version="2019-03-31")), (generate_enrollment_create_req(attestation_type='symmetricKey', primary_key='primarykey', secondary_key='secondarykey', @@ -174,7 +183,9 @@ def test_enrollment_create(self, serviceclient, req): req['reprovision_policy'], req['allocation_policy'], req['iot_hubs'], - req['edge_enabled']) + req['edge_enabled'], + req['webhook_url'], + req['api_version']) args = serviceclient.call_args url = args[0][0].url assert "{}/enrollments/{}?".format(mock_target['entity'], enrollment_id) in url @@ -225,6 +236,9 @@ def test_enrollment_create(self, serviceclient, req): assert not body['reprovisionPolicy']['updateHubAssignment'] if req['allocation_policy']: assert body['allocationPolicy'] == req['allocation_policy'] + if body['allocationPolicy'] == 'custom': + assert body['customAllocationDefinition']['webhookUrl'] == req['webhook_url'] + assert body['customAllocationDefinition']['apiVersion'] == req['api_version'] if req['iot_hubs']: assert body['iotHubs'] == req['iot_hubs'].split() if req['edge_enabled']: @@ -238,6 +252,8 @@ def test_enrollment_create(self, serviceclient, req): (generate_enrollment_create_req(reprovision_policy='invalid')), (generate_enrollment_create_req(allocation_policy='invalid')), (generate_enrollment_create_req(allocation_policy='static')), + (generate_enrollment_create_req(allocation_policy='custom')), + (generate_enrollment_create_req(allocation_policy='custom', webhook_url="https://www.test.test")), (generate_enrollment_create_req(allocation_policy='static', iot_hubs='hub1 hub2')), (generate_enrollment_create_req(allocation_policy='static', iot_hub_host_name='hubname')), (generate_enrollment_create_req(iot_hubs='hub1 hub2')) @@ -259,7 +275,8 @@ def test_enrollment_create_invalid_args(self, serviceclient, req): None, req['reprovision_policy'], req['allocation_policy'], - req['iot_hubs']) + req['iot_hubs'], + req['webhook_url']) @pytest.mark.parametrize("req", [ (generate_enrollment_create_req(attestation_type='tpm', endorsement_key='mykey')) @@ -308,7 +325,7 @@ def generate_enrollment_update_req(certificate_path=None, iot_hub_host_name=None device_id=None, etag=None, reprovision_policy=None, allocation_policy=None, iot_hubs=None, - edge_enabled=None): + edge_enabled=None, webhook_url=None, api_version=None): return {'client': None, 'enrollment_id': enrollment_id, 'rg': resource_group, @@ -326,7 +343,9 @@ def generate_enrollment_update_req(certificate_path=None, iot_hub_host_name=None 'reprovision_policy': reprovision_policy, 'allocation_policy': allocation_policy, 'iot_hubs': iot_hubs, - 'edge_enabled': edge_enabled} + 'edge_enabled': edge_enabled, + 'webhook_url': webhook_url, + 'api_version': api_version} class TestEnrollmentUpdate(): @@ -355,6 +374,9 @@ def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): (generate_enrollment_update_req(allocation_policy='static', iot_hubs='hub1')), (generate_enrollment_update_req(allocation_policy='hashed', iot_hubs='hub1 hub2')), (generate_enrollment_update_req(allocation_policy='geolatency')), + (generate_enrollment_update_req(allocation_policy='custom', + webhook_url="https://www.test.test", + api_version="2019-03-31")), (generate_enrollment_update_req(edge_enabled=True)), (generate_enrollment_update_req(edge_enabled=False)) ]) @@ -379,7 +401,9 @@ def test_enrollment_update(self, serviceclient, req): req['reprovision_policy'], req['allocation_policy'], req['iot_hubs'], - edge_enabled=req['edge_enabled']) + req['edge_enabled'], + req['webhook_url'], + req['api_version']) # Index 1 is the update args args = serviceclient.call_args_list[1] url = args[0][0].url @@ -419,6 +443,9 @@ def test_enrollment_update(self, serviceclient, req): assert not body['reprovisionPolicy']['updateHubAssignment'] if req['allocation_policy']: assert body['allocationPolicy'] == req['allocation_policy'] + if body['allocationPolicy'] == 'custom': + assert body['customAllocationDefinition']['webhookUrl'] == req['webhook_url'] + assert body['customAllocationDefinition']['apiVersion'] == req['api_version'] if req['iot_hubs']: assert body['iotHubs'] == req['iot_hubs'].split() if req['edge_enabled'] is not None: @@ -512,7 +539,9 @@ def generate_enrollment_group_create_req(iot_hub_host_name=None, reprovision_policy=None, allocation_policy=None, iot_hubs=None, - edge_enabled=False): + edge_enabled=False, + webhook_url=None, + api_version=None): return {'client': None, 'enrollment_id': enrollment_id, 'rg': resource_group, @@ -530,7 +559,9 @@ def generate_enrollment_group_create_req(iot_hub_host_name=None, 'reprovision_policy': reprovision_policy, 'allocation_policy': allocation_policy, 'iot_hubs': iot_hubs, - 'edge_enabled': edge_enabled} + 'edge_enabled': edge_enabled, + 'webhook_url': webhook_url, + 'api_version': api_version} class TestEnrollmentGroupCreate(): @@ -567,6 +598,10 @@ def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): iot_hubs='hub1 hub2')), (generate_enrollment_group_create_req(certificate_path='myCert', allocation_policy='geolatency')), + (generate_enrollment_group_create_req(certificate_path='myCert', + allocation_policy='custom', + webhook_url="https://www.test.test", + api_version="2019-03-31")), (generate_enrollment_group_create_req(primary_key='primarykey', secondary_key='secondarykey', edge_enabled=True)), @@ -589,7 +624,9 @@ def test_enrollment_group_create(self, serviceclient, req): req['reprovision_policy'], req['allocation_policy'], req['iot_hubs'], - edge_enabled=req['edge_enabled']) + req['edge_enabled'], + req['webhook_url'], + req['api_version']) args = serviceclient.call_args url = args[0][0].url assert "{}/enrollmentGroups/{}?".format(mock_target['entity'], enrollment_id) in url @@ -639,6 +676,9 @@ def test_enrollment_group_create(self, serviceclient, req): assert not body['reprovisionPolicy']['updateHubAssignment'] if req['allocation_policy']: assert body['allocationPolicy'] == req['allocation_policy'] + if body['allocationPolicy'] == 'custom': + assert body['customAllocationDefinition']['webhookUrl'] == req['webhook_url'] + assert body['customAllocationDefinition']['apiVersion'] == req['api_version'] if req['iot_hubs']: assert body['iotHubs'] == req['iot_hubs'].split() if req['edge_enabled']: @@ -654,6 +694,8 @@ def test_enrollment_group_create(self, serviceclient, req): secondary_certificate_path='myCert2')), (generate_enrollment_group_create_req(reprovision_policy='invalid')), (generate_enrollment_group_create_req(allocation_policy='invalid')), + (generate_enrollment_group_create_req(allocation_policy='custom')), + (generate_enrollment_group_create_req(allocation_policy='custom', webhook_url="https://www.test.test")), (generate_enrollment_group_create_req(allocation_policy='static', iot_hub_host_name='hub')), (generate_enrollment_group_create_req(allocation_policy='static', iot_hubs='hub1 hub2')), (generate_enrollment_group_create_req(iot_hubs='hub1 hub2')) @@ -676,7 +718,8 @@ def test_enrollment_group_create_invalid_args(self, serviceclient, req): req['provisioning_status'], req['reprovision_policy'], req['allocation_policy'], - req['iot_hubs']) + req['iot_hubs'], + webhook_url=req['webhook_url']) @pytest.mark.parametrize("req", [ (generate_enrollment_group_create_req(certificate_path='myCert')) @@ -735,7 +778,9 @@ def generate_enrollment_group_update_req(iot_hub_host_name=None, reprovision_policy=None, allocation_policy=None, iot_hubs=None, - edge_enabled=None): + edge_enabled=None, + webhook_url=None, + api_version=None): return {'client': None, 'enrollment_id': enrollment_id, 'rg': resource_group, @@ -756,7 +801,9 @@ def generate_enrollment_group_update_req(iot_hub_host_name=None, 'reprovision_policy': reprovision_policy, 'allocation_policy': allocation_policy, 'iot_hubs': iot_hubs, - 'edge_enabled': edge_enabled} + 'edge_enabled': edge_enabled, + 'webhook_url': webhook_url, + 'api_version': api_version} class TestEnrollmentGroupUpdate(): @@ -784,6 +831,9 @@ def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): (generate_enrollment_group_update_req(reprovision_policy='reprovisionandresetdata')), (generate_enrollment_group_update_req(reprovision_policy='never')), (generate_enrollment_group_update_req(allocation_policy='static', iot_hubs='hub1')), + (generate_enrollment_group_update_req(allocation_policy='custom', + webhook_url="https://www.test.test", + api_version="2019-03-31")), (generate_enrollment_group_update_req(allocation_policy='hashed', iot_hubs='hub1 hub2')), (generate_enrollment_group_update_req(allocation_policy='geolatency')), (generate_enrollment_group_update_req(iot_hub_host_name='hub1')), @@ -811,7 +861,9 @@ def test_enrollment_group_update(self, serviceclient, req): req['reprovision_policy'], req['allocation_policy'], req['iot_hubs'], - edge_enabled=req['edge_enabled']) + req['edge_enabled'], + req['webhook_url'], + req['api_version']) # Index 1 is the update args args = serviceclient.call_args_list[1] url = args[0][0].url @@ -854,6 +906,9 @@ def test_enrollment_group_update(self, serviceclient, req): assert not body['reprovisionPolicy']['updateHubAssignment'] if req['allocation_policy']: assert body['allocationPolicy'] == req['allocation_policy'] + if body['allocationPolicy'] == 'custom': + assert body['customAllocationDefinition']['webhookUrl'] == req['webhook_url'] + assert body['customAllocationDefinition']['apiVersion'] == req['api_version'] if req['iot_hubs']: assert body['iotHubs'] == req['iot_hubs'].split() if req['edge_enabled'] is not None: @@ -872,6 +927,8 @@ def test_enrollment_group_update(self, serviceclient, req): (generate_enrollment_group_update_req(remove_certificate='true')), (generate_enrollment_group_update_req(reprovision_policy='invalid')), (generate_enrollment_group_update_req(allocation_policy='invalid')), + (generate_enrollment_group_update_req(allocation_policy='custom')), + (generate_enrollment_group_update_req(allocation_policy='custom', webhook_url="https://www.test.test")), (generate_enrollment_group_update_req(allocation_policy='static', iot_hub_host_name='hub')), (generate_enrollment_group_update_req(allocation_policy='static', iot_hubs='hub1 hub2')), (generate_enrollment_group_update_req(iot_hubs='hub1 hub2')) @@ -897,7 +954,9 @@ def test_enrollment_group_update_invalid_args(self, serviceclient, req): req['provisioning_status'], req['reprovision_policy'], req['allocation_policy'], - req['iot_hubs']) + req['iot_hubs'], + None, + req['webhook_url']) class TestEnrollmentGroupShow(): From 919364d27bc550cca501dd702ef6101dc33bfe16 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Wed, 1 Jul 2020 18:27:00 -0700 Subject: [PATCH 059/179] Central - run command-response on device (#197) * add module level filtering - * Revert "add module level filtering -" This reverts commit 340a43c93e7575abaf22f493f3c548795ca02c74. * Implement method to call direct method on a device * Read command history * Added IT and some minor fixes * Updated help, minor fixes * minor update to comment * Changed command-response to execute-command * execute-command -> run-command * Response directly from API, added show-command * Minor rename Co-authored-by: Raj valluri --- azext_iot/central/_help.py | 45 +++++++++++ azext_iot/central/command_map.py | 2 + azext_iot/central/commands_device.py | 41 ++++++++++ azext_iot/central/params.py | 16 +++- .../central/providers/device_provider.py | 36 +++++++++ azext_iot/central/services/device.py | 76 +++++++++++++++++++ .../json/device_template_int_test.json | 28 +++++++ .../tests/central/json/sync_command_args.json | 5 ++ azext_iot/tests/test_iot_central_int.py | 70 +++++++++++++++-- 9 files changed, 311 insertions(+), 8 deletions(-) create mode 100644 azext_iot/tests/central/json/sync_command_args.json diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index f09b624d1..f3e9c4dd1 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -136,6 +136,51 @@ def _load_central_devices_help(): az iot central app device registration-summary --app-id {appid} """ + helps[ + "iot central app device run-command" + ] = """ + type: command + short-summary: Run a command on a device and view associated response. Does NOT monitor property updates that the command may perform. + long-summary: | + Note: payload should be nested under "request". + i.e. if your device expects the payload in a shape {"key": "value"} + payload should be {"request": {"key": "value"}}. + --content can be pointed at a filepath too (.../path/to/payload.json) + examples: + - name: Run command response + text: > + az iot central app device run-command + --app-id {appid} + --device-id {deviceid} + --interface-id {interfaceid} + --command-name {commandname} + --content {payload} + + - name: Short Run command response + text: > + az iot central app device run-command + -n {appid} + -d {deviceid} + -i {interfaceid} + --cn {commandname} + -k {payload} + """ + + helps[ + "iot central app device show-command-history" + ] = """ + type: command + short-summary: Get most recent command-response request and response payload. + examples: + - name: Show command response + text: > + az iot central app device show-command-history + --app-id {appid} + --device-id {deviceid} + --interface-id {interfaceid} + --command-name {commandname} + """ + def _load_central_device_templates_help(): helps[ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index f3f8baadb..3ebdd0860 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -45,6 +45,8 @@ def load_central_commands(self, _): cmd_group.command("show", "get_device") cmd_group.command("create", "create_device") cmd_group.command("delete", "delete_device") + cmd_group.command("run-command", "run_command") + cmd_group.command("show-command-history", "get_command_history") cmd_group.command("registration-info", "registration_info") cmd_group.command( "registration-summary", "registration_summary", diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py index a45e37b47..7da5317fa 100644 --- a/azext_iot/central/commands_device.py +++ b/azext_iot/central/commands_device.py @@ -6,6 +6,8 @@ # Dev note - think of this as a controller from knack.util import CLIError + +from azext_iot.common import utility from azext_iot.constants import CENTRAL_ENDPOINT from azext_iot.central.providers import CentralDeviceProvider @@ -63,6 +65,45 @@ def registration_info( ) +def run_command( + cmd, + app_id: str, + device_id: str, + interface_id: str, + command_name: str, + content: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, +): + 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) + return provider.run_component_command( + device_id=device_id, + interface_id=interface_id, + command_name=command_name, + payload=payload, + ) + + +def get_command_history( + cmd, + app_id: str, + device_id: str, + interface_id: str, + command_name: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, +): + provider = CentralDeviceProvider(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, + ) + + def registration_summary( cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 5136ea5c8..f847fd55c 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -32,7 +32,7 @@ def load_central_arguments(self, _): Load CLI Args for Knack parser """ with self.argument_context("iot central app") as context: - context.argument("app_id", options_list=["--app-id"], help="Target App.") + context.argument("app_id", options_list=["--app-id", "-n"], help="Target App.") context.argument("minimum_severity", arg_type=severity_type) context.argument( "instance_of", @@ -44,6 +44,16 @@ def load_central_arguments(self, _): options_list=["--device-name"], help="Human readable device name. Example: Fridge", ) + context.argument( + "interface_id", + options_list=["--interface-id", "-i"], + help="Interface name as specified in the device template. Example: c2dTestingTemplate_356", + ) + context.argument( + "command_name", + options_list=["--command-name", "--cn"], + help="Command name as specified in device template. Example: run_firmware_update", + ) context.argument( "simulated", options_list=["--simulated"], @@ -112,7 +122,7 @@ def load_central_arguments(self, _): # TODO: Delete this by end of July 2020 def load_deprecated_iotcentral_params(self, _): with self.argument_context("iotcentral") as context: - context.argument("app_id", options_list=["--app-id"], help="Target App.") + context.argument("app_id", options_list=["--app-id", "-n"], help="Target App.") context.argument("properties", arg_type=event_msg_prop_type) context.argument("timeout", arg_type=event_timeout_type) context.argument( @@ -167,7 +177,7 @@ def load_deprecated_iotcentral_params(self, _): ) with self.argument_context("iot central device-twin") as context: - context.argument("app_id", options_list=["--app-id"], help="Target App.") + context.argument("app_id", options_list=["--app-id", "-n"], help="Target App.") context.argument( "central_dns_suffix", options_list=["--central-dns-suffix", "--central-api-uri"], diff --git a/azext_iot/central/providers/device_provider.py b/azext_iot/central/providers/device_provider.py index cc0f87f38..cfa1eadca 100644 --- a/azext_iot/central/providers/device_provider.py +++ b/azext_iot/central/providers/device_provider.py @@ -190,6 +190,42 @@ def get_device_registration_summary(self, central_dns_suffix=CENTRAL_ENDPOINT): central_dns_suffix=central_dns_suffix, ) + def run_component_command( + self, + device_id: str, + interface_id: str, + command_name: str, + payload: dict, + central_dns_suffix=CENTRAL_ENDPOINT, + ): + 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, + ) + + def get_component_command_history( + self, + device_id: str, + interface_id: str, + command_name: str, + central_dns_suffix=CENTRAL_ENDPOINT, + ): + 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, + ) + def _dps_populate_essential_info(self, dps_info, device_status: DeviceStatus): error = { DeviceStatus.provisioned: "None.", diff --git a/azext_iot/central/services/device.py b/azext_iot/central/services/device.py index 1b5368c1b..1d9506bec 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -220,3 +220,79 @@ def get_device_credentials( response = requests.get(url, headers=headers) return _utility.try_extract_result(response) + + +def run_component_command( + cmd, + app_id: str, + token: str, + device_id: str, + interface_id: str, + command_name: str, + payload: dict, + central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + Execute a direct method on a device + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + device_id: unique case-sensitive device id + interface_id: interface id where command exists + command_name: name of command to execute + payload: params for command + 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 201) + """ + url = "https://{}.{}/{}/{}/components/{}/commands/{}".format( + app_id, central_dns_suffix, BASE_PATH, device_id, interface_id, command_name + ) + headers = _utility.get_headers(token, cmd) + + response = requests.post(url, headers=headers, json=payload) + + # 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 + if response.status_code == 201: + return response.json() + + return _utility.try_extract_result(response) + + +def get_component_command_history( + cmd, + app_id: str, + token: str, + device_id: str, + interface_id: str, + command_name: str, + central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + Get component command history + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + device_id: unique case-sensitive device id + interface_id: interface id where command exists + command_name: name of command to view execution history + 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: + Command history (List) - currently limited to 1 item + """ + url = "https://{}.{}/{}/{}/components/{}/commands/{}".format( + app_id, central_dns_suffix, BASE_PATH, device_id, interface_id, command_name + ) + headers = _utility.get_headers(token, cmd) + + response = requests.get(url, headers=headers) + return _utility.try_extract_result(response) 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 672320418..bb57afa16 100644 --- a/azext_iot/tests/central/json/device_template_int_test.json +++ b/azext_iot/tests/central/json/device_template_int_test.json @@ -228,6 +228,34 @@ "displayName": "Vector", "name": "Vector", "schema": "vector" + }, + { + "@id": "urn:sampleApp:modelOne_g4:sync_cmd: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" + } } ] } diff --git a/azext_iot/tests/central/json/sync_command_args.json b/azext_iot/tests/central/json/sync_command_args.json new file mode 100644 index 000000000..e47e4024e --- /dev/null +++ b/azext_iot/tests/central/json/sync_command_args.json @@ -0,0 +1,5 @@ +{ + "request": { + "argument": "value" + } +} \ No newline at end of file diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index ec998a39d..5724ac0bb 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -7,14 +7,20 @@ import os import time +from .conftest import get_context_path + from azure.cli.testsdk import LiveScenarioTest from azext_iot.central.models.enum import DeviceStatus from azext_iot.common import utility APP_ID = os.environ.get("azext_iot_central_app_id") -DEVICE_TEMPLATE_PATH = os.environ.get("azext_iot_central_device_template_path") -if not all([APP_ID, DEVICE_TEMPLATE_PATH]): +device_template_path = get_context_path( + __file__, "central/json/device_template_int_test.json" +) +sync_command_params = get_context_path(__file__, "central/json/sync_command_args.json") + +if not all([APP_ID, device_template_path]): raise ValueError( "Set azext_iot_central_app_id, azext_iot_central_device_template_path" "to run central integration tests. " @@ -190,6 +196,45 @@ def test_central_device_registration_info_registered(self): assert device_registration_info.get("status") is None assert dps_state.get("error") == "Device is not yet provisioned." + def test_central_run_command(self): + interface_id = "modelOne_g4" + command_name = "sync_cmd" + (template_id, _) = self._create_device_template() + (device_id, _) = self._create_device(instance_of=template_id, simulated=True) + + self._wait_for_provisioned(device_id) + + run_command_result = self.cmd( + "iot central app device run-command" + " -n {}" + " -d {}" + " -i {}" + " --cn {}" + " -k '{}'".format( + APP_ID, device_id, interface_id, command_name, sync_command_params + ) + ) + + show_command_result = self.cmd( + "iot central app device show-command-history" + " -n {}" + " -d {}" + " -i {}" + " --cn {}".format(APP_ID, device_id, interface_id, command_name) + ) + + self._delete_device(device_id) + self._delete_device_template(template_id) + + run_result = run_command_result.get_output_in_json() + show_result = show_command_result.get_output_in_json() + + # from file indicated by `sync_command_params` + assert run_result["request"] == {"argument": "value"} + + # check that run result and show result indeed match + assert run_result["response"] == show_result["value"][0]["response"] + def test_central_device_registration_info_unassociated(self): (device_id, device_name) = self._create_device() @@ -274,6 +319,21 @@ def _create_device(self, **kwargs) -> (str, str): self.cmd(command, checks=checks) return (device_id, device_name) + def _wait_for_provisioned(self, device_id): + command = "iot central app device show --app-id {} -d {}".format( + APP_ID, device_id + ) + while True: + result = self.cmd(command) + device = result.get_output_in_json() + + # return when its provisioned + if device.get("provisioned"): + return + + # wait 10 seconds for provisioning to complete + time.sleep(10) + def _delete_device(self, device_id) -> None: self.cmd( "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), @@ -282,14 +342,14 @@ def _delete_device(self, device_id) -> None: def _create_device_template(self): template = utility.process_json_arg( - DEVICE_TEMPLATE_PATH, argument_name="DEVICE_TEMPLATE_PATH" + device_template_path, argument_name="device_template_path" ) template_name = template["displayName"] template_id = template_name + "id" self.cmd( - "iot central app device-template create --app-id {} --device-template-id {} -k {}".format( - APP_ID, template_id, DEVICE_TEMPLATE_PATH + "iot central app device-template create --app-id {} --device-template-id {} -k '{}'".format( + APP_ID, template_id, device_template_path ), checks=[ self.check("displayName", template_name), From 851caa1c8a4ca7c2fde1c7f794bef2b00dccc234 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 2 Jul 2020 16:55:47 -0700 Subject: [PATCH 060/179] Add package init structural check to reduce last minute issues. (#211) --- azext_iot/common/utility.py | 4 +-- azext_iot/tests/test_iot_utility_unit.py | 35 ++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index 1e8692987..8c34f25cb 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -471,9 +471,7 @@ def ensure_min_version(cur_ver, min_ver): def scantree(path): - from os import scandir - - for entry in scandir(path): + for entry in os.scandir(path): if entry.is_dir(follow_symlinks=False): yield from scantree(entry.path) else: diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index 215e52eb6..23d7f523b 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -7,6 +7,7 @@ import json import mock import pytest +import os from knack.util import CLIError from azure.cli.core.extension import get_extension_path @@ -281,7 +282,7 @@ def test_file_json_fail_invalidcontent(self, content, argname, set_cwd, mocker): assert mocked_util_logger.call_count == 0 -class TestVersionComparison: +class TestVersionComparison(object): @pytest.mark.parametrize( "current, minimum, expected", [ @@ -297,7 +298,7 @@ def test_ensure_min_version(self, current, minimum, expected): assert ensure_min_version(current, minimum) == expected -class TestEmbeddedCli: +class TestEmbeddedCli(object): @pytest.fixture(params=[0, 1]) def mocked_azclient(self, mocker, request): def mock_invoke(args, out_file): @@ -313,7 +314,10 @@ def mock_invoke(args, out_file): "command, subscription", [ ("iot hub device-identity create -n abcd -d dcba", None), - ("iot hub device-twin show -n 'abcd' -d 'dcba'", "20a300e5-a444-4130-bb5a-1abd08ad930a"), + ( + "iot hub device-twin show -n 'abcd' -d 'dcba'", + "20a300e5-a444-4130-bb5a-1abd08ad930a", + ), ], ) def test_embedded_cli(self, mocked_azclient, command, subscription): @@ -341,3 +345,28 @@ def test_embedded_cli(self, mocked_azclient, command, subscription): assert cli.output assert cli.as_json() + + +class TestCliInit(object): + def test_package_init(self): + from azext_iot.constants import EXTENSION_ROOT + + tests_root = "tests" + directory_structure = {} + + def _validate_directory(path): + for entry in os.scandir(path): + if entry.is_dir(follow_symlinks=False) and all( + [not entry.name.startswith("__"), tests_root not in entry.path] + ): + directory_structure[entry.path] = None + _validate_directory(entry.path) + else: + if entry.path.endswith("__init__.py"): + directory_structure[os.path.dirname(entry.path)] = entry.path + + _validate_directory(EXTENSION_ROOT) + + for directory in directory_structure: + if directory_structure[directory] is None: + pytest.fail("Directory: '{}' missing __init__.py".format(directory)) From fcd4864742b8e27af6bcd646b0c8f68a01dd1c55 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Wed, 8 Jul 2020 10:29:00 -0700 Subject: [PATCH 061/179] Add IT's for central validate-messages (#209) * Implemented telemetry:model validation logic * Lint * Added unit tests, minor code refactor * remove pony name * Added schema_type tests * Fix lint issue * lint * Added interface handling, TODO: validate error cases * Interface parsing is now working * Fix UT and lint issue * lint * lint * Refactor code and tests to make more sense * fix UT * first iteration of validation IT's * Added happy path test * Finished tests, remove redundant bindings from pytest.ini.example * Add help for new command, fix lint * Beefed up central monitor-events test * Moved from string literals to string functions * get-credentials -> show-credentials, remove CI var --- .../templates/set-testenv-sentinel.yml | 1 - azext_iot/central/_help.py | 14 ++ azext_iot/central/command_map.py | 1 + azext_iot/central/commands_device.py | 9 + azext_iot/monitor/handlers/central_handler.py | 6 +- azext_iot/tests/__init__.py | 34 +-- azext_iot/tests/helpers.py | 36 ++++ azext_iot/tests/test_iot_central_int.py | 197 ++++++++++++++++-- dev_requirements | 1 + pytest.ini.example | 2 - 10 files changed, 261 insertions(+), 40 deletions(-) diff --git a/.azure-devops/templates/set-testenv-sentinel.yml b/.azure-devops/templates/set-testenv-sentinel.yml index 79f477681..c71ecb90f 100644 --- a/.azure-devops/templates/set-testenv-sentinel.yml +++ b/.azure-devops/templates/set-testenv-sentinel.yml @@ -17,7 +17,6 @@ steps: "azext_pnp_repository", "azext_pnp_cs", "azext_iot_central_app_id", - "azext_iot_central_device_template_path", ] f = open("./pytest.ini", "w+") f.write("[pytest]\n") diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index f3e9c4dd1..738c5cb11 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -107,6 +107,20 @@ def _load_central_devices_help(): --device-id {deviceid} """ + helps[ + "iot central app device show-credentials" + ] = """ + type: command + short-summary: Get device credentials from IoT Central + + examples: + - name: Get device credentials for a device + text: > + az iot central app device show-credentials + --app-id {appid} + --device-id {deviceid} + """ + helps[ "iot central app device registration-info" ] = """ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index 3ebdd0860..7c884259a 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -51,6 +51,7 @@ def load_central_commands(self, _): cmd_group.command( "registration-summary", "registration_summary", ) + cmd_group.command("show-credentials", "get_credentials") with self.command_group( "iot central app device-template", diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py index 7da5317fa..e1a45012b 100644 --- a/azext_iot/central/commands_device.py +++ b/azext_iot/central/commands_device.py @@ -111,3 +111,12 @@ def registration_summary( return provider.get_device_registration_summary( central_dns_suffix=central_dns_suffix, ) + + +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,) + return provider.get_device_credentials( + device_id=device_id, central_dns_suffix=central_dns_suffix, + ) diff --git a/azext_iot/monitor/handlers/central_handler.py b/azext_iot/monitor/handlers/central_handler.py index d2b277d8b..210c896b6 100644 --- a/azext_iot/monitor/handlers/central_handler.py +++ b/azext_iot/monitor/handlers/central_handler.py @@ -76,15 +76,15 @@ def validate_message(self, message): self._print_progress_update(n_messages) + if self._central_handler_args.style == "scroll" and issues: + [issue.log() for issue in issues] + if ( self._central_handler_args.max_messages and n_messages >= self._central_handler_args.max_messages ): self._quit_messages_exceeded() - if self._central_handler_args.style == "scroll" and issues: - [issue.log() for issue in issues] - def generate_startup_string(self, name: str): device_id = self._central_handler_args.common_handler_args.device_id duration = self._central_handler_args.duration diff --git a/azext_iot/tests/__init__.py b/azext_iot/tests/__init__.py index f2e50768e..7580605a6 100644 --- a/azext_iot/tests/__init__.py +++ b/azext_iot/tests/__init__.py @@ -50,7 +50,25 @@ def close(self): buffer_tee.close() -class IoTLiveScenarioTest(LiveScenarioTest): +class CaptureOutputLiveScenarioTest(LiveScenarioTest): + def __init__(self, test_scenario): + super(CaptureOutputLiveScenarioTest, self).__init__(test_scenario) + + # TODO: @digimaun - Maybe put a helper like this in the shared lib, when you create it? + def command_execute_assert(self, command, asserts): + from . import capture_output + + with capture_output() as buffer: + self.cmd(command, checks=None) + output = buffer.get_output() + + for a in asserts: + assert a in output + + return output + + +class IoTLiveScenarioTest(CaptureOutputLiveScenarioTest): def __init__(self, test_scenario, entity_name, entity_rg, entity_cs): assert test_scenario assert entity_name @@ -95,21 +113,9 @@ def generate_config_names(self, count=1, edge=False): def generate_job_names(self, count=1): return [ - self.create_random_name(prefix=PREFIX_JOB, length=32) - for i in range(count) + self.create_random_name(prefix=PREFIX_JOB, length=32) for i in range(count) ] - # TODO: @digimaun - Maybe put a helper like this in the shared lib, when you create it? - def command_execute_assert(self, command, asserts): - from . import capture_output - - with capture_output() as buffer: - self.cmd(command, checks=None) - output = buffer.get_output() - - for a in asserts: - assert a in output - def tearDown(self): if self.device_ids: device = self.device_ids.pop() diff --git a/azext_iot/tests/helpers.py b/azext_iot/tests/helpers.py index 494ba4870..e500c8bd6 100644 --- a/azext_iot/tests/helpers.py +++ b/azext_iot/tests/helpers.py @@ -8,10 +8,46 @@ import os from inspect import getsourcefile +from azure.iot.device import ProvisioningDeviceClient, IoTHubDeviceClient from azext_iot.common.utility import read_file_content +GLOBAL_PROVISIONING_HOST = "global.azure-devices-provisioning.net" + def load_json(filename): os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) return json.loads(read_file_content(filename)) + + +def dps_connect_device(device_id: str, credentials: dict) -> IoTHubDeviceClient: + id_scope = credentials["idScope"] + key = credentials["symmetricKey"]["primaryKey"] + + provisioning_device_client = ProvisioningDeviceClient.create_from_symmetric_key( + provisioning_host=GLOBAL_PROVISIONING_HOST, + registration_id=device_id, + id_scope=id_scope, + symmetric_key=key, + ) + + registration_result = provisioning_device_client.register() + if registration_result.status == "assigned": + device_client = IoTHubDeviceClient.create_from_symmetric_key( + symmetric_key=key, + hostname=registration_result.registration_state.assigned_hub, + device_id=registration_result.registration_state.device_id, + ) + device_client.connect() + return device_client + + +class MockLogger: + def info(self, msg): + print(msg) + + def warn(self, msg): + print(msg) + + def error(self, msg): + print(msg) diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 5724ac0bb..cceea463c 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -4,14 +4,19 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- + +import json import os import time from .conftest import get_context_path -from azure.cli.testsdk import LiveScenarioTest -from azext_iot.central.models.enum import DeviceStatus +from azure.iot.device import Message from azext_iot.common import utility +from azext_iot.central.models.enum import DeviceStatus +from azext_iot.monitor.parsers import strings + +from . import CaptureOutputLiveScenarioTest, helpers APP_ID = os.environ.get("azext_iot_central_app_id") @@ -20,16 +25,13 @@ ) sync_command_params = get_context_path(__file__, "central/json/sync_command_args.json") -if not all([APP_ID, device_template_path]): - raise ValueError( - "Set azext_iot_central_app_id, azext_iot_central_device_template_path" - "to run central integration tests. " - ) +if not all([APP_ID]): + raise ValueError("Set azext_iot_central_app_id to run central integration tests.") -class TestIotCentral(LiveScenarioTest): - def __init__(self, test_case): - super(TestIotCentral, self).__init__(test_case) +class TestIotCentral(CaptureOutputLiveScenarioTest): + def __init__(self, test_scenario): + super(TestIotCentral, self).__init__(test_scenario=test_scenario) def test_central_device_twin_show_fail(self): (device_id, _) = self._create_device() @@ -91,24 +93,138 @@ def test_central_device_twin_show_success(self): self._delete_device_template(template_id) def test_central_monitor_events(self): + (template_id, _) = self._create_device_template() + (device_id, _) = self._create_device(instance_of=template_id) + credentials = self._get_credentials(device_id) + + device_client = helpers.dps_connect_device(device_id, credentials) + + payload = {"Bool": True} + msg = Message( + data=json.dumps(payload), + content_encoding="utf-8", + content_type="application/json", + ) + device_client.send_message(msg) + + enqueued_time = utility.calculate_millisec_since_unix_epoch_utc() - 10000 + # Test with invalid app-id self.cmd( "iotcentral app monitor-events --app-id {}".format(APP_ID + "zzz"), expect_failure=True, ) + # Ensure no failure - # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices - self.cmd("iotcentral app monitor-events --app-id {} --to 1".format(APP_ID)) + output = self._get_monitor_events_output(device_id, enqueued_time) - def test_central_validate_messages(self): - # Test with invalid app-id - self.cmd( - "iot central app validate-messages --app-id {}".format(APP_ID + "zzz"), - expect_failure=True, + self._delete_device(device_id) + self._delete_device_template(template_id) + assert '"Bool": true' in output + assert device_id in output + + def test_central_validate_messages_success(self): + (template_id, _) = self._create_device_template() + (device_id, _) = self._create_device(instance_of=template_id) + credentials = self._get_credentials(device_id) + + device_client = helpers.dps_connect_device(device_id, credentials) + + enqueued_time = utility.calculate_millisec_since_unix_epoch_utc() - 10000 + + payload = {"Bool": True} + msg = Message( + data=json.dumps(payload), + content_encoding="utf-8", + content_type="application/json", ) - # Ensure no failure - # We cannot verify that the result is correct, as the Azure CLI for IoT Central does not support adding devices - self.cmd("iot central app validate-messages --app-id {} --to 1".format(APP_ID)) + device_client.send_message(msg) + + # Validate the messages + output = self._get_validate_messages_output(device_id, enqueued_time) + + self._delete_device(device_id) + self._delete_device_template(template_id) + + assert output + assert "Successfully parsed 1 message(s)" in output + assert "No errors detected" in output + + 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) + credentials = self._get_credentials(device_id) + + device_client = helpers.dps_connect_device(device_id, credentials) + + enqueued_time = utility.calculate_millisec_since_unix_epoch_utc() - 10000 + + # Invalid encoding + payload = {"Bool": True} + msg = Message(data=json.dumps(payload), content_type="application/json") + device_client.send_message(msg) + expected_messages.append(strings.invalid_encoding("")) + + # Content type mismatch (e.g. non application/json) + payload = {"Bool": True} + msg = Message(data=json.dumps(payload), content_encoding="utf-8") + device_client.send_message(msg) + expected_messages.append(strings.content_type_mismatch("", "application/json")) + + # Invalid type + payload = {"Bool": 123} + msg = Message( + data=json.dumps(payload), + content_encoding="utf-8", + content_type="application/json", + ) + device_client.send_message(msg) + expected_messages.append( + strings.invalid_primitive_schema_mismatch_template("Bool", "boolean", 123) + ) + + # Telemetry not defined + payload = {"NotPresentInTemplate": True} + msg = Message( + data=json.dumps(payload), + content_encoding="utf-8", + content_type="application/json", + ) + device_client.send_message(msg) + # this error is harder to build from strings because we have to construct a whole template for it + expected_messages.append( + "Following capabilities have NOT been defined in the device template '['NotPresentInTemplate']'" + ) + + # Invalid JSON + payload = '{"asd":"def}' + msg = Message( + data=payload, content_encoding="utf-8", content_type="application/json", + ) + device_client.send_message(msg) + expected_messages.append(strings.invalid_json()) + + # Validate the messages + output = self._get_validate_messages_output( + device_id, enqueued_time, max_messages=len(expected_messages) + ) + + self._delete_device(device_id) + self._delete_device_template(template_id) + + assert output + + expected_issues = [ + "No encoding found. Expected encoding 'utf-8' to be present in message header.", + "Content type '' is not supported. Expected Content type is 'application/json'.", + "Datatype of field 'Bool' does not match the datatype 'boolean'.", + "Data '123'. All dates/times/datetimes/durations must be ISO 8601 compliant.", + "Following capabilities have NOT been defined in the device template '['NotPresentInTemplate']'", + "Invalid JSON format", + ] + for issue in expected_issues: + assert issue in output def test_central_device_methods_CRLD(self): (device_id, device_name) = self._create_device() @@ -366,3 +482,44 @@ def _delete_device_template(self, template_id): ), checks=[self.check("result", "success")], ) + + def _get_credentials(self, device_id): + return self.cmd( + "iot central app device show-credentials --app-id {} -d {}".format( + APP_ID, device_id + ) + ).get_output_in_json() + + def _get_validate_messages_output( + self, device_id, enqueued_time, duration=60, max_messages=1, asserts=None + ): + if not asserts: + asserts = [] + + output = self.command_execute_assert( + "iot central app validate-messages --app-id {} -d {} --et {} --duration {} --mm {} -y --style json".format( + APP_ID, device_id, enqueued_time, duration, max_messages + ), + asserts, + ) + + if not output: + output = "" + + return output + + def _get_monitor_events_output(self, device_id, enqueued_time, asserts=None): + if not asserts: + asserts = [] + + output = self.command_execute_assert( + "iot central app monitor-events -n {} -d {} --et {} --to 1".format( + APP_ID, device_id, enqueued_time + ), + asserts, + ) + + if not output: + output = "" + + return output diff --git a/dev_requirements b/dev_requirements index 16e42bcf1..02e41c0dc 100644 --- a/dev_requirements +++ b/dev_requirements @@ -11,3 +11,4 @@ pre-commit six>=1.12 pylint flake8 +azure-iot-device diff --git a/pytest.ini.example b/pytest.ini.example index 6d8bd7e0c..d2d09a564 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -23,8 +23,6 @@ env = azext_pnp_repository= azext_pnp_cs= azext_iot_central_app_id= - azext_iot_central_device_id= - azext_iot_central_device_template_path=./azext_iot/tests/central/json/device_template_int_test.json azext_dt_rbac_assignee_owner= azext_dt_rbac_assignee_reader= azext_dt_ep_eventgrid_topic= From 8da7a85c198d6d02e82673263f8690eacbe5bc19 Mon Sep 17 00:00:00 2001 From: valluriraj Date: Fri, 10 Jul 2020 12:58:19 -0700 Subject: [PATCH 062/179] Central property monitor V2 (#212) * add module level filtering - * Revert "add module level filtering -" This reverts commit 340a43c93e7575abaf22f493f3c548795ca02c74. * initial commit * additional fixes * additional changes * additional changes * Refactored * removed code * remove more code * propoerty monitoring updates * unit test updates * lint fixes * Revert "lint fixes" This reverts commit 00305d1f2fe6e817029d47d48a6ce706367658f1. * lint fixes * additional fixes * lint fixes and UT * fixed gettwin UT * initial commit * validation of property * additional changes * unit test updats * additional changes * address review comments and update unit tests * address review comments * address review comments Co-authored-by: Raj valluri Co-authored-by: prbans Co-authored-by: Paul Montgomery --- azext_iot/central/_help.py | 16 ++ azext_iot/central/command_map.py | 1 + azext_iot/central/commands_monitor.py | 21 +- azext_iot/central/models/template.py | 16 +- azext_iot/central/params.py | 1 - azext_iot/central/providers/__init__.py | 7 +- azext_iot/constants.py | 1 + azext_iot/monitor/parsers/central_parser.py | 2 +- azext_iot/monitor/parsers/strings.py | 8 + azext_iot/monitor/property.py | 226 +++++++++++++----- .../json/property_validation_template.json | 161 +++++++++++++ azext_iot/tests/test_constants.py | 3 + azext_iot/tests/test_iot_central_unit.py | 154 +++++++++++- 13 files changed, 546 insertions(+), 71 deletions(-) create mode 100644 azext_iot/tests/central/json/property_validation_template.json diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 738c5cb11..ff29b4db1 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -390,6 +390,22 @@ def _load_central_monitors_help(): az iot central app monitor-properties --app-id {app_id} -d {device_id} """ + helps[ + "iot central app validate-properties" + ] = """ + type: command + short-summary: Validate reported properties sent to IoT Central app. + long-summary: | + Performs validations on reported property updates: + 1) Warning - Properties sent by device that are not modeled in central. + 2) Warning - Properties with same name declared in multiple interfaces + should have interface name included as part of the property update. + examples: + - name: Basic usage + text: > + az iot central app validate-properties --app-id {app_id} -d {device_id} + """ + # TODO: Delete this by July 2020 def _load_central_deprecated_commands(): diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index 7c884259a..af4e7ff45 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -37,6 +37,7 @@ def load_central_commands(self, _): cmd_group.command("monitor-events", "monitor_events") cmd_group.command("validate-messages", "validate_messages", is_preview=True) cmd_group.command("monitor-properties", "monitor_properties", is_preview=True) + cmd_group.command("validate-properties", "validate_properties", is_preview=True) with self.command_group( "iot central app device", command_type=central_device_ops, is_preview=True, diff --git a/azext_iot/central/commands_monitor.py b/azext_iot/central/commands_monitor.py index fa49b0594..b7c9b1cf6 100644 --- a/azext_iot/central/commands_monitor.py +++ b/azext_iot/central/commands_monitor.py @@ -15,7 +15,7 @@ CentralHandlerArguments, TelemetryArguments, ) -from azext_iot.monitor.property import start_property_monitor +from azext_iot.monitor.property import PropertyMonitor def validate_messages( @@ -119,9 +119,26 @@ def monitor_events( def monitor_properties( cmd, device_id, app_id, central_dns_suffix=CENTRAL_ENDPOINT, ): - start_property_monitor( + monitor = PropertyMonitor( cmd=cmd, + app_id=app_id, device_id=device_id, + central_dns_suffix=central_dns_suffix, + ) + monitor.start_property_monitor() + + +def validate_properties( + cmd, + device_id, + app_id, + central_dns_suffix=CENTRAL_ENDPOINT, + minimum_severity=Severity.warning.name, +): + monitor = PropertyMonitor( + cmd=cmd, app_id=app_id, + device_id=device_id, central_dns_suffix=central_dns_suffix, ) + monitor.start_validate_property_monitor(Severity[minimum_severity]) diff --git a/azext_iot/central/models/template.py b/azext_iot/central/models/template.py index cbaf2d4f9..fe631ccd7 100644 --- a/azext_iot/central/models/template.py +++ b/azext_iot/central/models/template.py @@ -18,15 +18,15 @@ def __init__(self, template: dict): except: raise CLIError("Could not parse iot central device template.") - def get_schema(self, telemetry_name, interface_name=""): + def get_schema(self, name, interface_name=""): # interface_name has been specified, do a pointed lookup if interface_name: interface = self.interfaces.get(interface_name, {}) - return interface.get(telemetry_name) + return interface.get(name) - # find first matching telemetry_name in any interface + # find first matching name in any interface for interface in self.interfaces.values(): - schema = interface.get(telemetry_name) + schema = interface.get(name) if schema: return schema @@ -55,3 +55,11 @@ def _extract_schema_names(self, interfaces: dict) -> dict: interface_name: list(interface_schemas.keys()) for interface_name, interface_schemas in interfaces.items() } + + def _get_interface_list_property(self, property_name): + # 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 f847fd55c..5003f204c 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -114,7 +114,6 @@ def load_central_arguments(self, _): help="Maximum number of messages to recieve from target device before terminating connection." "Use 0 for infinity.", ) - # TODO: Delete this by end of July 2020 load_deprecated_iotcentral_params(self, _) diff --git a/azext_iot/central/providers/__init__.py b/azext_iot/central/providers/__init__.py index 57e791421..afd1eb98c 100644 --- a/azext_iot/central/providers/__init__.py +++ b/azext_iot/central/providers/__init__.py @@ -8,5 +8,10 @@ from azext_iot.central.providers.device_template_provider import ( CentralDeviceTemplateProvider, ) +from azext_iot.central.providers.devicetwin_provider import CentralDeviceTwinProvider -__all__ = ["CentralDeviceProvider", "CentralDeviceTemplateProvider"] +__all__ = [ + "CentralDeviceProvider", + "CentralDeviceTemplateProvider", + "CentralDeviceTwinProvider", +] diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 496ed201d..6d69f9432 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -48,6 +48,7 @@ DEVICETWIN_MONITOR_TIME_SEC = 15 # (Lib name, minimum version (including), maximum version (excluding)) EVENT_LIB = ("uamqp", "1.2", "1.3") +CENTRAL_PNP_INTERFACE_PREFIX = "$iotin:" # Config Key's CONFIG_KEY_UAMQP_EXT_VERSION = "uamqp_ext_version" diff --git a/azext_iot/monitor/parsers/central_parser.py b/azext_iot/monitor/parsers/central_parser.py index 11e2cad8a..5e8001564 100644 --- a/azext_iot/monitor/parsers/central_parser.py +++ b/azext_iot/monitor/parsers/central_parser.py @@ -125,7 +125,7 @@ def _validate_payload_against_interfaces( name_miss = [] for telemetry_name, telemetry in payload.items(): schema = template.get_schema( - telemetry_name=telemetry_name, interface_name=self.interface_name + name=telemetry_name, interface_name=self.interface_name ) if not schema: name_miss.append(telemetry_name) diff --git a/azext_iot/monitor/parsers/strings.py b/azext_iot/monitor/parsers/strings.py index 54ac4379c..f62668f9a 100644 --- a/azext_iot/monitor/parsers/strings.py +++ b/azext_iot/monitor/parsers/strings.py @@ -73,6 +73,14 @@ def invalid_field_name_mismatch_template( ).format(unmodeled_capabilities, modeled_capabilities) +# warning +def duplicate_property_name(duplicate_prop_name, interfaces: list): + return ( + "Duplicate property: '{}' found under following interfaces {} in the device model. " + "Either provide the interface name as part of the device payload or make the propery name unique in the device model" + ).format(duplicate_prop_name, interfaces) + + # error def invalid_primitive_schema_mismatch_template(field_name: str, data_type: str, data): return ( diff --git a/azext_iot/monitor/property.py b/azext_iot/monitor/property.py index adcbf0b13..ed694a053 100644 --- a/azext_iot/monitor/property.py +++ b/azext_iot/monitor/property.py @@ -7,91 +7,201 @@ import datetime import isodate import time - +from azext_iot.monitor.parsers import strings +from azext_iot.monitor.models.enum import Severity from azext_iot.constants import ( CENTRAL_ENDPOINT, DEVICETWIN_POLLING_INTERVAL_SEC, DEVICETWIN_MONITOR_TIME_SEC, + CENTRAL_PNP_INTERFACE_PREFIX, ) -from azext_iot.central.providers.devicetwin_provider import CentralDeviceTwinProvider + from azext_iot.central.models.devicetwin import DeviceTwin, Property +from azext_iot.central.providers import ( + CentralDeviceProvider, + CentralDeviceTemplateProvider, + CentralDeviceTwinProvider, +) +from azext_iot.monitor.parsers.issue import IssueHandler + + +class PropertyMonitor: + def __init__( + self, cmd, app_id, device_id, central_dns_suffix=CENTRAL_ENDPOINT, + ): + self._cmd = cmd + self._app_id = app_id + self._device_id = device_id + self._central_dns_suffix = central_dns_suffix + self._device_twin_provider = CentralDeviceTwinProvider( + cmd=self._cmd, app_id=self._app_id, device_id=self._device_id + ) + self._central_device_provider = CentralDeviceProvider(self._cmd, self._app_id) + self._central_template_provider = CentralDeviceTemplateProvider( + cmd=self._cmd, app_id=self._app_id + ) + self._template = self._get_device_template() + def _compare_properties(self, prev_prop: Property, prop: Property): + if prev_prop.version == prop.version: + return -def start_property_monitor( - cmd, - device_id, - app_id, - central_dns_suffix=CENTRAL_ENDPOINT, - polling_interval_seconds=DEVICETWIN_POLLING_INTERVAL_SEC, -): - prev_twin = None + changes = { + key: self._changed_props(prop.props[key], prop.metadata[key], key,) + for key, val in prop.metadata.items() + if self._is_relevant(key, val) + } - device_twin_provider = CentralDeviceTwinProvider( - cmd=cmd, app_id=app_id, device_id=device_id - ) + return changes - while True: + def _is_relevant(self, key, val): + if key in {"$lastUpdated", "$lastUpdatedVersion"}: + return False - raw_twin = device_twin_provider.get_device_twin( - central_dns_suffix=central_dns_suffix + updated_within = datetime.datetime.now() - datetime.timedelta( + seconds=DEVICETWIN_MONITOR_TIME_SEC ) - twin = DeviceTwin(raw_twin) - if prev_twin: - change_d = compare_properties( - prev_twin.desired_property, twin.desired_property + last_updated = isodate.parse_datetime(val["$lastUpdated"]) + return last_updated.timestamp() >= updated_within.timestamp() + + def _changed_props(self, prop, metadata, property_name): + # not an interface - whole thing is change log + if not self._is_interface(property_name): + return prop + # iterate over property in the interface + # if the property is not an exact match for what is present in the previous set of properties + # track it as a change + diff = { + key: prop[key] + for key, val in metadata.items() + if self._is_relevant(key, val) + } + return diff + + def _validate_payload(self, changes, minimum_severity): + for value in changes: + issues = self._validate_payload_against_interfaces( + changes[value], value, minimum_severity + ) + for issue in issues: + issue.log() + + def _validate_payload_against_interfaces( + self, payload: dict, name, minimum_severity + ): + name_miss = [] + issues_handler = IssueHandler() + interface_name = name.replace(CENTRAL_PNP_INTERFACE_PREFIX, "") + if self._is_interface(interface_name): + # if the payload is an interface then iterate thru the properties under the interface + for property_name in payload: + schema = self._template.get_schema( + name=property_name, interface_name=interface_name + ) + if not schema: + name_miss.append(property_name) + else: + # if the payload is a property then process the payload as a single unit. + schema = self._template.get_schema(name=name) + + if not schema: + name_miss.append(name) + + interfaces_with_specified_property = self._template._get_interface_list_property( + name + ) + + if len(interfaces_with_specified_property) > 1: + details = strings.duplicate_property_name( + name, interfaces_with_specified_property + ) + issues_handler.add_central_issue( + severity=Severity.warning, + details=details, + message=None, + device_id=self._device_id, + template_id=self._template.id, + ) + + if name_miss: + details = strings.invalid_field_name_mismatch_template( + name_miss, self._template.schema_names ) - change_r = compare_properties( - prev_twin.reported_property, twin.reported_property + issues_handler.add_central_issue( + severity=Severity.warning, + details=details, + message=None, + device_id=self._device_id, + template_id=self._template.id, ) - if change_d: - print("Changes in desired properties:") - print("version :", twin.desired_property.version) - print(change_d) + return issues_handler.get_issues_with_minimum_severity(minimum_severity) - if change_r: - print("Changes in reported properties:") - print("version :", twin.reported_property.version) - print(change_r) + def _is_interface(self, interface_name): + # Remove PNP interface prefix to get the actual interface name + interface_name_modified = interface_name.replace( + CENTRAL_PNP_INTERFACE_PREFIX, "" + ) + return interface_name_modified in self._template.interfaces - time.sleep(polling_interval_seconds) + def _get_device_template(self): - prev_twin = twin + device = self._central_device_provider.get_device(self._device_id) + template = self._central_template_provider.get_device_template( + device_template_id=device.instance_of, + central_dns_suffix=self._central_dns_suffix, + ) + return template + def start_property_monitor(self,): + prev_twin = None -def compare_properties(prev_prop: Property, prop: Property): - if prev_prop.version == prop.version: - return + while True: - changes = { - key: changed_props(prop.props[key], prop.metadata[key], key) - for key, val in prop.metadata.items() - if is_relevant(key, val) - } + raw_twin = self._device_twin_provider.get_device_twin( + central_dns_suffix=self._central_dns_suffix + ) - return changes + twin = DeviceTwin(raw_twin) + if prev_twin: + change_d = self._compare_properties( + prev_twin.desired_property, twin.desired_property, + ) + change_r = self._compare_properties( + prev_twin.reported_property, twin.reported_property + ) + if change_d: + print("Changes in desired properties:") + print("version :", twin.desired_property.version) + print(change_d) -def is_relevant(key, val): - if key in {"$lastUpdated", "$lastUpdatedVersion"}: - return False + if change_r: + print("Changes in reported properties:") + print("version :", twin.reported_property.version) + print(change_r) + time.sleep(DEVICETWIN_POLLING_INTERVAL_SEC) - updated_within = datetime.datetime.now() - datetime.timedelta( - seconds=DEVICETWIN_MONITOR_TIME_SEC - ) + prev_twin = twin - last_updated = isodate.parse_datetime(val["$lastUpdated"]) - return last_updated.timestamp() >= updated_within.timestamp() + def start_validate_property_monitor(self, minimum_severity): + prev_twin = None + + while True: + + raw_twin = self._device_twin_provider.get_device_twin( + central_dns_suffix=self._central_dns_suffix + ) + twin = DeviceTwin(raw_twin) + if prev_twin: + change_r = self._compare_properties( + prev_twin.reported_property, twin.reported_property + ) + if change_r: + self._validate_payload(change_r, minimum_severity) -def changed_props(prop, metadata, property_name): - # not an interface - whole thing is change log - if "$iotin" not in property_name: - return prop + time.sleep(DEVICETWIN_POLLING_INTERVAL_SEC) - # iterate over property in the interface - # if the property has been updated within DEVICETWIN_MONITOR_TIME_SEC - # track it as a change - diff = {key: prop[key] for key, val in metadata.items() if is_relevant(key, val)} - return diff + prev_twin = twin diff --git a/azext_iot/tests/central/json/property_validation_template.json b/azext_iot/tests/central/json/property_validation_template.json new file mode 100644 index 000000000..4c3530f21 --- /dev/null +++ b/azext_iot/tests/central/json/property_validation_template.json @@ -0,0 +1,161 @@ +{ + "id": "urn:d9cltbeus:tvj4oal1a0", + "etag": "\"~WgqHZmg+d95gTA53P8AnqBsDLGgj2wa0msOL7xozC9Y=\"", + "types": [ + "DeviceModel" + ], + "displayName": "property-validation", + "capabilityModel": { + "@id": "urn:sampleApp:groupOne_bz:2", + "@type": [ + "CapabilityModel" + ], + "implements": [ + { + "@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" + } + ] + } + }, + { + "@id": "urn:sampleApp:groupTwo_bz:myxqftpsr:2", + "@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" + } + ] + } + }, + { + "@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" + } + ] + } + } + ], + "displayName": "property_validation", + "@context": [ + "http://azureiot.com/v1/contexts/IoTModel.json" + ] + }, + "solutionModel": { + "@id": "urn:d9cltbeus:lz1tl4a_jz", + "@type": [ + "SolutionModel" + ], + "cloudProperties": [], + "initialValues": [], + "overrides": [] + } +} \ No newline at end of file diff --git a/azext_iot/tests/test_constants.py b/azext_iot/tests/test_constants.py index c5b5fef61..d39a00250 100644 --- a/azext_iot/tests/test_constants.py +++ b/azext_iot/tests/test_constants.py @@ -12,3 +12,6 @@ class FileNames: ) central_device_file = "central/json/device.json" central_device_twin_file = "central/json/device_twin.json" + central_property_validation_template_file = ( + "central/json/property_validation_template.json" + ) diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index 0617815b0..0ca9b0aa8 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -19,7 +19,9 @@ CentralDeviceTemplateProvider, ) from azext_iot.central.models.devicetwin import DeviceTwin -from azext_iot.monitor.property import compare_properties +from azext_iot.central.models.template import Template +from azext_iot.monitor.property import PropertyMonitor +from azext_iot.monitor.models.enum import Severity from .helpers import load_json from .test_constants import FileNames @@ -157,7 +159,6 @@ def test_monitor_events_invalid_args(self, timeout, exception, fixture_cmd): class TestCentralDeviceProvider: _device = load_json(FileNames.central_device_file) _device_template = load_json(FileNames.central_device_template_file) - _device_twin = load_json(FileNames.central_device_twin_file) @mock.patch("azext_iot.central.services.device_template") @mock.patch("azext_iot.central.services.device") @@ -202,6 +203,13 @@ def test_should_return_device_template( assert mock_device_template_svc.get_device_template.call_count == 1 assert template == self._device_template + +class TestCentralPropertyMonitor: + _device_twin = load_json(FileNames.central_device_twin_file) + _duplicate_property_template = load_json( + FileNames.central_property_validation_template_file + ) + @mock.patch("azext_iot.central.services.device_template") @mock.patch("azext_iot.central.services.device") def test_should_return_updated_properties( @@ -216,7 +224,12 @@ def test_should_return_updated_properties( twin = DeviceTwin(raw_twin) twin_next = DeviceTwin(raw_twin) twin_next.reported_property.version = twin.reported_property.version + 1 - result = compare_properties(twin_next.reported_property, twin.reported_property) + monitor = PropertyMonitor( + cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + ) + result = monitor._compare_properties( + twin_next.reported_property, twin.reported_property + ) assert len(result) == 3 assert len(result["$iotin:urn_azureiot_Client_SDKInformation"]) == 3 assert result["$iotin:urn_azureiot_Client_SDKInformation"]["language"] @@ -249,5 +262,138 @@ def test_should_return_no_properties( ) twin = DeviceTwin(raw_twin) twin_next = DeviceTwin(raw_twin) - result = compare_properties(twin_next.reported_property, twin.reported_property) + monitor = PropertyMonitor( + cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + ) + result = monitor._compare_properties( + twin_next.reported_property, twin.reported_property + ) assert result is None + + @mock.patch("azext_iot.central.services.device_template") + @mock.patch("azext_iot.central.services.device") + def test_validate_properties_declared_multiple_interfaces( + self, mock_device_svc, mock_device_template_svc + ): + + # setup + mock_device_template_svc.get_device_template.return_value = Template( + self._duplicate_property_template + ) + + monitor = PropertyMonitor( + cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + ) + + model = {"Model": "test_model"} + + issues = monitor._validate_payload_against_interfaces( + model, list(model.keys())[0], Severity.warning, + ) + + assert ( + issues[0].details == "Duplicate property: 'Model' found under following " + "interfaces ['groupOne_g4', 'groupTwo_ed', 'groupThree_ed'] " + "in the device model. Either provide the interface name as part " + "of the device payload or make the propery name unique in the device model" + ) + + version = {"OsName": "test_osName"} + + issues = monitor._validate_payload_against_interfaces( + version, list(version.keys())[0], Severity.warning, + ) + + assert len(issues) == 0 + + @mock.patch("azext_iot.central.services.device_template") + @mock.patch("azext_iot.central.services.device") + def test_validate_properties_name_miss( + self, mock_device_svc, mock_device_template_svc + ): + + # setup + mock_device_template_svc.get_device_template.return_value = Template( + self._duplicate_property_template + ) + + monitor = PropertyMonitor( + cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + ) + + # invalid interface / property + definition = {"definition": "test_definition"} + + issues = monitor._validate_payload_against_interfaces( + definition, list(definition.keys())[0], Severity.warning, + ) + + assert ( + issues[0].details + == "Device is sending data that has not been defined in the device " + "template. Following capabilities have NOT been defined in the device template " + "'['definition']'. Following capabilities have been defined in the device template " + "(grouped by interface) '{'groupOne_g4': ['Model', 'Version', 'TotalStorage'], " + "'groupTwo_ed': ['Model', 'Manufacturer'], 'groupThree_ed': ['Manufacturer', " + "'Version', 'Model', 'OsName']}'. " + ) + + # invalid and valid property with valid interface + property_collection = { + "Model": "test_model", + "Manufacturer": "test_manufacturer", + "OsName": "test_osName", + } + + issues = monitor._validate_payload_against_interfaces( + property_collection, "groupOne_g4", Severity.warning, + ) + + assert ( + issues[0].details + == "Device is sending data that has not been defined in the device " + "template. Following capabilities have NOT been defined in the device template " + "'['Manufacturer', 'OsName']'. Following capabilities have been defined in the device template " + "(grouped by interface) '{'groupOne_g4': ['Model', 'Version', 'TotalStorage'], " + "'groupTwo_ed': ['Model', 'Manufacturer'], 'groupThree_ed': ['Manufacturer', " + "'Version', 'Model', 'OsName']}'. " + ) + + @mock.patch("azext_iot.central.services.device_template") + @mock.patch("azext_iot.central.services.device") + def test_validate_properties_severity_level( + self, mock_device_svc, mock_device_template_svc + ): + + # setup + mock_device_template_svc.get_device_template.return_value = Template( + self._duplicate_property_template + ) + + monitor = PropertyMonitor( + cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + ) + + # severity level info + definition = {"definition": "test_definition"} + + issues = monitor._validate_payload_against_interfaces( + definition, list(definition.keys())[0], Severity.info, + ) + + assert ( + issues[0].details + == "Device is sending data that has not been defined in the device " + "template. Following capabilities have NOT been defined in the device template " + "'['definition']'. Following capabilities have been defined in the device template " + "(grouped by interface) '{'groupOne_g4': ['Model', 'Version', 'TotalStorage'], " + "'groupTwo_ed': ['Model', 'Manufacturer'], 'groupThree_ed': ['Manufacturer', " + "'Version', 'Model', 'OsName']}'. " + ) + + # severity level error + issues = monitor._validate_payload_against_interfaces( + definition, list(definition.keys())[0], Severity.error, + ) + + assert len(issues) == 0 From a56d78e3e4dc882efad665f8c45e7a642cac4c05 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 14 Jul 2020 18:35:50 -0700 Subject: [PATCH 063/179] PnP summer refresh (#216) Refresh commands for PnP Model Repository and Digital Twin Model APIs * Merged PR 35: PnP Summer refresh updates * PnP runtime updates for summer refresh. * Adds pnp runtime unit tests. * Allow --pnp-dns-suffix, Test updates and slight refactoring * Added parameter for connecting to secondary PnP repo environments * Added eTag fetch logic to model repo provider * Updated int/unit tests and help accordingly * Added int test skip logic if user lacks correct permissions * Added parameter processing for --top command * Resolve set of PR feedback. Co-authored-by: Paymaun Co-authored-by: Ryan Kelly --- .../templates/set-testenv-sentinel.yml | 3 - azext_iot/__init__.py | 21 +- azext_iot/_factory.py | 57 +- azext_iot/_help.py | 299 ------ azext_iot/_params.py | 66 +- azext_iot/_validators.py | 3 - azext_iot/commands.py | 32 - azext_iot/common/_azure.py | 89 -- azext_iot/constants.py | 6 +- azext_iot/iothub/_help.py | 62 +- .../{command_bindings.py => command_map.py} | 14 +- .../{job_commands.py => commands_job.py} | 0 azext_iot/iothub/commands_pnp_runtime.py | 64 ++ azext_iot/iothub/device_commands.py | 16 - azext_iot/iothub/params.py | 32 + azext_iot/iothub/providers/device_identity.py | 23 - azext_iot/iothub/providers/pnp_runtime.py | 101 ++ azext_iot/monitor/parsers/common_parser.py | 29 +- azext_iot/operations/digitaltwin.py | 389 ------- azext_iot/operations/pnp.py | 234 ----- azext_iot/pnp/__init__.py | 9 + azext_iot/pnp/_help.py | 145 +++ azext_iot/pnp/command_map.py | 39 + azext_iot/pnp/commands_api.py | 59 ++ azext_iot/pnp/commands_repository.py | 56 ++ azext_iot/pnp/common.py | 59 ++ azext_iot/pnp/params.py | 115 +++ azext_iot/pnp/providers/__init__.py | 65 ++ .../pnp/providers/model_repository_api.py | 89 ++ azext_iot/pnp/providers/resource.py | 71 ++ azext_iot/sdk/iothub/pnp_runtime/__init__.py | 18 + .../iot_hub_gateway_service_ap_is.py | 91 ++ .../pnp_runtime/models/__init__.py} | 3 - .../iothub/pnp_runtime/operations/__init__.py | 17 + .../operations/digital_twin_operations.py | 332 ++++++ azext_iot/sdk/iothub/pnp_runtime/version.py | 13 + azext_iot/sdk/pnp/__init__.py | 19 +- azext_iot/sdk/pnp/dataplane/__init__.py | 18 + .../digital_twin_model_repository_api.py | 428 ++++++++ .../pnp/{ => dataplane}/models/__init__.py | 12 +- .../pnp/dataplane/models/model_information.py | 112 +++ .../dataplane/models/model_information_py3.py | 112 +++ .../dataplane/models/model_search_options.py | 47 + .../models/model_search_options_py3.py | 47 + .../models/service_error.py} | 23 +- .../pnp/dataplane/models/service_error_py3.py | 32 + azext_iot/sdk/pnp/dataplane/version.py | 13 + .../pnp/digital_twin_repository_service.py | 361 ------- azext_iot/sdk/pnp/modelrepository/__init__.py | 18 + .../model_repository_control_plane_api.py | 479 +++++++++ .../pnp/modelrepository/models/__init__.py | 28 + .../sdk/pnp/modelrepository/models/subject.py | 40 + .../models/subject_metadata.py | 32 + .../models/subject_metadata_py3.py | 32 + .../pnp/modelrepository/models/subject_py3.py | 40 + .../sdk/pnp/modelrepository/models/target.py | 32 + .../pnp/modelrepository/models/target_py3.py | 32 + .../sdk/pnp/modelrepository/models/tenant.py | 51 + .../pnp/modelrepository/models/tenant_py3.py | 51 + azext_iot/sdk/pnp/modelrepository/version.py | 13 + azext_iot/sdk/pnp/models/model_information.py | 72 -- .../sdk/pnp/models/model_information_py3.py | 72 -- azext_iot/sdk/pnp/models/search_options.py | 42 - .../sdk/pnp/models/search_options_py3.py | 42 - .../sdk/pnp/models/search_response_py3.py | 33 - .../iothub/jobs/test_iothub_jobs_unit.py | 2 +- .../tests/iothub/pnp_runtime/__init__.py | 5 + .../pnp_runtime/test_iot_pnp_runtime_unit.py | 239 +++++ azext_iot/tests/pnp/__init__.py | 15 + azext_iot/tests/pnp/test_iot_pnp_int.py | 303 ++++++ azext_iot/tests/pnp/test_iot_pnp_unit.py | 515 ++++++++++ .../test_pnp_create_payload_interface.json | 0 .../pnp/test_pnp_create_payload_model.json | 13 + .../{ => pnp}/test_pnp_interface_show.json | 0 azext_iot/tests/test_iot_digitaltwin_unit.py | 611 ----------- azext_iot/tests/test_iot_pnp_int.py | 237 ----- azext_iot/tests/test_iot_pnp_unit.py | 950 ------------------ azext_iot/tests/test_monitor_parsers_unit.py | 53 +- .../tests/test_pnp_create_payload_model.json | 10 - azext_iot/tests/test_pnp_interface_list.json | 17 - pytest.ini.example | 3 - 81 files changed, 4332 insertions(+), 3695 deletions(-) rename azext_iot/iothub/{command_bindings.py => command_map.py} (51%) rename azext_iot/iothub/{job_commands.py => commands_job.py} (100%) create mode 100644 azext_iot/iothub/commands_pnp_runtime.py delete mode 100644 azext_iot/iothub/device_commands.py create mode 100644 azext_iot/iothub/params.py delete mode 100644 azext_iot/iothub/providers/device_identity.py create mode 100644 azext_iot/iothub/providers/pnp_runtime.py delete mode 100644 azext_iot/operations/digitaltwin.py delete mode 100644 azext_iot/operations/pnp.py create mode 100644 azext_iot/pnp/__init__.py create mode 100644 azext_iot/pnp/_help.py create mode 100644 azext_iot/pnp/command_map.py create mode 100644 azext_iot/pnp/commands_api.py create mode 100644 azext_iot/pnp/commands_repository.py create mode 100644 azext_iot/pnp/common.py create mode 100644 azext_iot/pnp/params.py create mode 100644 azext_iot/pnp/providers/__init__.py create mode 100644 azext_iot/pnp/providers/model_repository_api.py create mode 100644 azext_iot/pnp/providers/resource.py create mode 100644 azext_iot/sdk/iothub/pnp_runtime/__init__.py create mode 100644 azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py rename azext_iot/sdk/{pnp/version.py => iothub/pnp_runtime/models/__init__.py} (96%) create mode 100644 azext_iot/sdk/iothub/pnp_runtime/operations/__init__.py create mode 100644 azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py create mode 100644 azext_iot/sdk/iothub/pnp_runtime/version.py create mode 100644 azext_iot/sdk/pnp/dataplane/__init__.py create mode 100644 azext_iot/sdk/pnp/dataplane/digital_twin_model_repository_api.py rename azext_iot/sdk/pnp/{ => dataplane}/models/__init__.py (72%) create mode 100644 azext_iot/sdk/pnp/dataplane/models/model_information.py create mode 100644 azext_iot/sdk/pnp/dataplane/models/model_information_py3.py create mode 100644 azext_iot/sdk/pnp/dataplane/models/model_search_options.py create mode 100644 azext_iot/sdk/pnp/dataplane/models/model_search_options_py3.py rename azext_iot/sdk/pnp/{models/search_response.py => dataplane/models/service_error.py} (51%) create mode 100644 azext_iot/sdk/pnp/dataplane/models/service_error_py3.py create mode 100644 azext_iot/sdk/pnp/dataplane/version.py delete mode 100644 azext_iot/sdk/pnp/digital_twin_repository_service.py create mode 100644 azext_iot/sdk/pnp/modelrepository/__init__.py create mode 100644 azext_iot/sdk/pnp/modelrepository/model_repository_control_plane_api.py create mode 100644 azext_iot/sdk/pnp/modelrepository/models/__init__.py create mode 100644 azext_iot/sdk/pnp/modelrepository/models/subject.py create mode 100644 azext_iot/sdk/pnp/modelrepository/models/subject_metadata.py create mode 100644 azext_iot/sdk/pnp/modelrepository/models/subject_metadata_py3.py create mode 100644 azext_iot/sdk/pnp/modelrepository/models/subject_py3.py create mode 100644 azext_iot/sdk/pnp/modelrepository/models/target.py create mode 100644 azext_iot/sdk/pnp/modelrepository/models/target_py3.py create mode 100644 azext_iot/sdk/pnp/modelrepository/models/tenant.py create mode 100644 azext_iot/sdk/pnp/modelrepository/models/tenant_py3.py create mode 100644 azext_iot/sdk/pnp/modelrepository/version.py delete mode 100644 azext_iot/sdk/pnp/models/model_information.py delete mode 100644 azext_iot/sdk/pnp/models/model_information_py3.py delete mode 100644 azext_iot/sdk/pnp/models/search_options.py delete mode 100644 azext_iot/sdk/pnp/models/search_options_py3.py delete mode 100644 azext_iot/sdk/pnp/models/search_response_py3.py create mode 100644 azext_iot/tests/iothub/pnp_runtime/__init__.py create mode 100644 azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py create mode 100644 azext_iot/tests/pnp/__init__.py create mode 100644 azext_iot/tests/pnp/test_iot_pnp_int.py create mode 100644 azext_iot/tests/pnp/test_iot_pnp_unit.py rename azext_iot/tests/{ => pnp}/test_pnp_create_payload_interface.json (100%) create mode 100644 azext_iot/tests/pnp/test_pnp_create_payload_model.json rename azext_iot/tests/{ => pnp}/test_pnp_interface_show.json (100%) delete mode 100644 azext_iot/tests/test_iot_digitaltwin_unit.py delete mode 100644 azext_iot/tests/test_iot_pnp_int.py delete mode 100644 azext_iot/tests/test_iot_pnp_unit.py delete mode 100644 azext_iot/tests/test_pnp_create_payload_model.json delete mode 100644 azext_iot/tests/test_pnp_interface_list.json diff --git a/.azure-devops/templates/set-testenv-sentinel.yml b/.azure-devops/templates/set-testenv-sentinel.yml index c71ecb90f..88de2ae57 100644 --- a/.azure-devops/templates/set-testenv-sentinel.yml +++ b/.azure-devops/templates/set-testenv-sentinel.yml @@ -13,9 +13,6 @@ steps: "azext_iot_testhub_cs", "azext_iot_testrg", "azext_iot_testdps", - "azext_pnp_endpoint", - "azext_pnp_repository", - "azext_pnp_cs", "azext_iot_central_app_id", ] f = open("./pytest.ini", "w+") diff --git a/azext_iot/__init__.py b/azext_iot/__init__.py index ff0b70d6f..557a6f753 100644 --- a/azext_iot/__init__.py +++ b/azext_iot/__init__.py @@ -12,24 +12,11 @@ iothub_ops = CliCommandType(operations_tmpl="azext_iot.operations.hub#{}") - -iothub_ops_job = CliCommandType(operations_tmpl="azext_iot.iothub.job_commands#{}") - -iothub_ops_device = CliCommandType( - operations_tmpl="azext_iot.iothub.device_commands#{}" -) - iotdps_ops = CliCommandType( operations_tmpl="azext_iot.operations.dps#{}", client_factory=iot_service_provisioning_factory, ) -iotdigitaltwin_ops = CliCommandType( - operations_tmpl="azext_iot.operations.digitaltwin#{}" -) - -iotpnp_ops = CliCommandType(operations_tmpl="azext_iot.operations.pnp#{}") - class IoTExtCommandsLoader(AzCommandsLoader): def __init__(self, cli_ctx=None): @@ -37,25 +24,31 @@ def __init__(self, cli_ctx=None): def load_command_table(self, args): from azext_iot.commands import load_command_table - from azext_iot.iothub.command_bindings import load_iothub_commands + from azext_iot.iothub.command_map import load_iothub_commands from azext_iot.central.command_map import load_central_commands from azext_iot.digitaltwins.command_map import load_digitaltwins_commands + from azext_iot.pnp.command_map import load_pnp_commands load_command_table(self, args) load_iothub_commands(self, args) load_central_commands(self, args) load_digitaltwins_commands(self, args) + load_pnp_commands(self, args) return self.command_table def load_arguments(self, command): from azext_iot._params import load_arguments + from azext_iot.iothub.params import load_iothub_arguments from azext_iot.central.params import load_central_arguments from azext_iot.digitaltwins.params import load_digitaltwins_arguments + from azext_iot.pnp.params import load_pnp_arguments load_arguments(self, command) + load_iothub_arguments(self, command) load_central_arguments(self, command) load_digitaltwins_arguments(self, command) + load_pnp_arguments(self, command) COMMAND_LOADER_CLS = IoTExtCommandsLoader diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py index d302f973c..20c9f633f 100644 --- a/azext_iot/_factory.py +++ b/azext_iot/_factory.py @@ -18,7 +18,7 @@ "iot_service_provisioning_factory", "SdkResolver", "_bind_sdk", - "_get_sdk_exception_type" + "_get_sdk_exception_type", ] @@ -59,6 +59,7 @@ def iot_service_provisioning_factory(cli_ctx, *_): """ from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.mgmt.iothubprovisioningservices.iot_dps_client import IotDpsClient + return get_mgmt_service_client(cli_ctx, IotDpsClient) @@ -80,58 +81,69 @@ def get_sdk(self, sdk_type): def _construct_sdk_map(self): return { SdkType.service_sdk: self._get_iothub_service_sdk, # Don't need to call here - SdkType.device_sdk: self._get_iothub_device_sdk + SdkType.device_sdk: self._get_iothub_device_sdk, + SdkType.pnp_sdk: self._get_pnp_runtime_sdk, } def _get_iothub_device_sdk(self): from azext_iot.sdk.iothub.device import IotHubGatewayDeviceAPIs + credentials = SasTokenAuthentication( uri=self.sas_uri, - shared_access_policy_name=self.target['policy'], - shared_access_key=self.target['primarykey']) + shared_access_policy_name=self.target["policy"], + shared_access_key=self.target["primarykey"], + ) return IotHubGatewayDeviceAPIs(credentials=credentials, base_url=self.endpoint) def _get_iothub_service_sdk(self): from azext_iot.sdk.iothub.service import IotHubGatewayServiceAPIs + credentials = SasTokenAuthentication( uri=self.sas_uri, - shared_access_policy_name=self.target['policy'], - shared_access_key=self.target['primarykey']) + shared_access_policy_name=self.target["policy"], + shared_access_key=self.target["primarykey"], + ) + + return IotHubGatewayServiceAPIs(credentials=credentials, base_url=self.endpoint) + + def _get_pnp_runtime_sdk(self): + from azext_iot.sdk.iothub.pnp_runtime import IotHubGatewayServiceAPIs + + 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) # TODO: Deprecated. To be removed asap. def _bind_sdk(target, sdk_type, device_id=None, auth=None): - from azext_iot.sdk.service.iot_hub_gateway_service_apis import IotHubGatewayServiceAPIs + from azext_iot.sdk.service.iot_hub_gateway_service_apis import ( + IotHubGatewayServiceAPIs, + ) from azext_iot.sdk.dps import ProvisioningServiceClient - from azext_iot.sdk.pnp.digital_twin_repository_service import DigitalTwinRepositoryService - sas_uri = target['entity'] + sas_uri = target["entity"] endpoint = "https://{}".format(sas_uri) if device_id: - sas_uri = '{}/devices/{}'.format(sas_uri, device_id) - - if sdk_type is SdkType.pnp_sdk: - return ( - DigitalTwinRepositoryService(endpoint), - _get_sdk_exception_type(sdk_type) - ) + sas_uri = "{}/devices/{}".format(sas_uri, device_id) if not auth: - auth = SasTokenAuthentication(sas_uri, target['policy'], target['primarykey']) + auth = SasTokenAuthentication(sas_uri, target["policy"], target["primarykey"]) if sdk_type is SdkType.service_sdk: return ( IotHubGatewayServiceAPIs(auth, endpoint), - _get_sdk_exception_type(sdk_type) + _get_sdk_exception_type(sdk_type), ) if sdk_type is SdkType.dps_sdk: return ( ProvisioningServiceClient(auth, endpoint), - _get_sdk_exception_type(sdk_type) + _get_sdk_exception_type(sdk_type), ) return None @@ -142,8 +154,9 @@ def _get_sdk_exception_type(sdk_type): from importlib import import_module exception_library = { - SdkType.service_sdk: import_module('msrestazure.azure_exceptions'), - SdkType.dps_sdk: import_module('azext_iot.sdk.dps.models.provisioning_service_error_details'), - SdkType.pnp_sdk: import_module('msrest.exceptions') + SdkType.service_sdk: import_module("msrestazure.azure_exceptions"), + SdkType.dps_sdk: import_module( + "azext_iot.sdk.dps.models.provisioning_service_error_details" + ), } return exception_library.get(sdk_type, None) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index b2d395cfb..66cfa937e 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -1124,302 +1124,3 @@ type: command short-summary: Delete a device registration in an Azure IoT Hub Device Provisioning Service. """ - - -helps[ - "iot dt" -] = """ - type: group - short-summary: Manage digital twin of an IoT Plug and Play device. -""" - -helps[ - "iot dt invoke-command" -] = """ - type: command - short-summary: Executes a command on an IoT Plug and Play device. - long-summary: You can leverage az login and provide --hub-name instead of --login for every command. - examples: - - name: Execute a command on device . - text: > - az iot dt invoke-command --login {iothub_cs} - --interface {plug_and_play_interface} --device-id {device_id} - --command-name {command_name} --command-payload {payload} - - name: Execute a command on device within current session. - text: > - az iot dt invoke-command --hub-name {iothub_name} - --interface {plug_and_play_interface} --device-id {device_id} - --command-name {command_name} --command-payload {payload} -""" - -helps[ - "iot dt list-interfaces" -] = """ - type: command - short-summary: List interfaces of a target IoT Plug and Play device. - long-summary: You can leverage az login and provide --hub-name instead of --login for every command. - examples: - - name: List all IoT Plug and Play interfaces on a device. - text: > - az iot dt list-interfaces --login {iothub_cs} - --device-id {device_id} - - name: List all IoT Plug and Play interfaces on a device within current session. - text: > - az iot dt list-interfaces --hub-name {iothub_name} --device-id {device_id} -""" - -helps[ - "iot dt list-properties" -] = """ - type: command - short-summary: List properties of a target IoT Plug and Play device interface(s). - long-summary: You can leverage az login and provide --hub-name instead of --login for every command. - examples: - - name: List all properties of all device's interfaces on an IoT Plug and Play device. - text: > - az iot dt list-properties --login {iothub_cs} --source device - --device-id {device_id} - - name: List all properties of all public interfaces on an IoT Plug and Play device within current session. - text: > - az iot dt list-properties --hub-name {iothub_name} --device-id {device_id} --source public - - name: List all properties of device's interface on an IoT Plug and Play device. - text: > - az iot dt list-properties --login {iothub_cs} --source device - --device-id {device_id} --interface {plug_and_play_interface} -""" - -helps[ - "iot dt list-commands" -] = """ - type: command - short-summary: List commands of an IoT Plug and Play devices interface(s). - long-summary: You can leverage az login and provide --hub-name instead of --login for every command. - examples: - - name: List all commands of all private interfaces on an IoT Plug and Play device. - text: > - az iot dt list-commands --login {iothub_cs} --source private - --device-id {device_id} --repo-id {plug_and_play_model_repository_id} - - name: List all commands of a private interface on an IoT Plug and Play device. - text: > - az iot dt list-commands --login {iothub_cs} --source private - --device-id {device_id} --repo-id {plug_and_play_model_repository_id} - --interface {plug_and_play_interface} - - name: List all commands of all public interfaces on an IoT Plug and Play device. - text: > - az iot dt list-commands --login {iothub_cs} --source public - --device-id {device_id} - - name: List all commands of device's interface on an IoT Plug and Play device. - text: > - az iot dt list-commands --login {iothub_cs} --source device - --device-id {device_id} --interface {plug_and_play_interface} -""" - -helps[ - "iot dt monitor-events" -] = """ - type: command - short-summary: Monitor Digital Twin events. - long-summary: You can leverage az login and provide --hub-name instead of --login for every command. - examples: - - name: Basic usage monitoring events of all devices and all interfaces using the logged in session. - text: > - az iot dt monitor-events -n {iothub_name} - - name: Basic usage monitoring events of all devices and all interfaces using an IotHub connection string. - text: > - az iot dt monitor-events --login {iothub_cs} - - name: Basic usage when filtering on specific interface events while targeting devices with a wildcard in the ID. - text: > - az iot dt monitor-events -n {iothub_name} -d Device* -i {plug_and_play_interface} - - name: Filter Digital Twin events of a subset of devices using IoT Hub query language. - text: > - az iot dt monitor-events -n {iothub_name} -q "select * from devices where tags.location.region = 'US'" - - name: Filter events on a device with a particular interface. Use a custom consumer group when binding and - see all message properties. - text: > - az iot dt monitor-events --login {iothub_cs} --device-id {device_id} - --interface {plug_and_play_interface} --consumer-group {consumer_group_name} --properties all -""" - -helps[ - "iot dt update-property" -] = """ - type: command - short-summary: Update an IoT Plug and Play device interfaces writable property. - examples: - - name: Update an IoT Plug and Play device interfaces read-write property. - text: > - az iot dt update-property --login {iothub_cs} --device-id {device_id} - --interface-payload {payload} - - name: Update an IoT Plug and Play device interfaces read-write property within current session. - text: > - az iot dt update-property --hub-name {iothub_name} --device-id {device_id} - --interface-payload {payload} -""" - -helps[ - "iot pnp" -] = """ - type: group - short-summary: Manage entities of an IoT Plug and Play model repository. -""" - -helps[ - "iot pnp interface" -] = """ - type: group - short-summary: Manage interfaces in an IoT Plug and Play model repository. -""" - -helps[ - "iot pnp interface publish" -] = """ - type: command - short-summary: Publish an interface to public repository. - examples: - - name: Publish an interface to public repository. - text: > - az iot pnp interface publish -r {pnp_repository} --interface {plug_and_play_interface_id} -""" - -helps[ - "iot pnp interface create" -] = """ - type: command - short-summary: Create an interface in the company repository. - examples: - - name: Create an interface in the company repository. - text: > - az iot pnp interface create --def {plug_and_play_interface_file_path} -r {pnp_repository} -""" - -helps[ - "iot pnp interface update" -] = """ - type: command - short-summary: Update an interface in the company repository. - examples: - - name: Update an interface in the company repository. - text: > - az iot pnp interface update --def {updated_plug_and_play_interface_file_path} -r {pnp_repository} -""" - -helps[ - "iot pnp interface list" -] = """ - type: command - short-summary: List all interfaces. - examples: - - name: List all company repository's interfaces. - text: > - az iot pnp interface list -r {pnp_repository} - - name: List all public interfaces. - text: > - az iot pnp interface list -""" - -helps[ - "iot pnp interface show" -] = """ - type: command - short-summary: Get the details of an interface. - examples: - - name: Get the details of a company repository interface. - text: > - az iot pnp interface show -r {pnp_repository} --interface {plug_and_play_interface_id} - - name: Get the details of public interface. - text: > - az iot pnp interface show --interface {plug_and_play_interface_id} -""" - -helps[ - "iot pnp interface delete" -] = """ - type: command - short-summary: Delete an interface in the company repository. - examples: - - name: Delete an interface in the company repository. - text: > - az iot pnp interface delete -r {pnp_repository} --interface {plug_and_play_interface_id} -""" - -helps[ - "iot pnp capability-model" -] = """ - type: group - short-summary: Manage device capability models in an IoT Plug and Play model repository. -""" - -helps[ - "iot pnp capability-model list" -] = """ - type: command - short-summary: List all capability-model. - examples: - - name: List all company repository's capability-model. - text: > - az iot pnp capability-model list -r {pnp_repository} - - name: List all public capability-model. - text: > - az iot pnp capability-model list -""" - -helps[ - "iot pnp capability-model show" -] = """ - type: command - short-summary: Get the details of a capability-model. - examples: - - name: Get the details of a company repository capability-model. - text: > - az iot pnp capability-model show -r {pnp_repository} --model {plug_and_play_capability_model_id} - - name: Get the details of public capability-model. - text: > - az iot pnp capability-model show --model {plug_and_play_capability_model_id} -""" - -helps[ - "iot pnp capability-model create" -] = """ - type: command - short-summary: Create a capability-model in the company repository. - examples: - - name: Create a capability-model in the company repository. - text: > - az iot pnp capability-model create --def {plug_and_play_capability_model_file_path} -r {pnp_repository} -""" - -helps[ - "iot pnp capability-model publish" -] = """ - type: command - short-summary: Publish the capability-model to public repository. - examples: - - name: Publish the capability-model to public repository. - text: > - az iot pnp capability-model publish -r {pnp_repository} - --model {plug_and_play_capability_model_id} -""" - -helps[ - "iot pnp capability-model delete" -] = """ - type: command - short-summary: Delete the capability-model in the company repository. - examples: - - name: Delete the capability-model in the company repository. - text: > - az iot pnp capability-model delete -r {pnp_repository} - --model {plug_and_play_capability_model_id} -""" - -helps[ - "iot pnp capability-model update" -] = """ - type: command - short-summary: Update the capability-model in the company repository. - examples: - - name: Update the capability-model in the company repository. - text: > - az iot pnp capability-model update --def {updated_plug_and_play_capability_model_file_path} - -r {pnp_repository} -""" diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 7133cc3f2..de588c10b 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -335,7 +335,7 @@ def load_arguments(self, _): context.argument( "interface", options_list=["--interface", "-i"], - help="Target interface name. This should be the name of the interface not the urn-id.", + help="Target interface identifier to filter on. For example: dtmi:com:example:TemperatureController;1", ) with self.argument_context("iot hub monitor-feedback") as context: @@ -911,67 +911,3 @@ def load_arguments(self, _): with self.argument_context("iot dt monitor-events") as context: context.argument("timeout", arg_type=event_timeout_type) context.argument("properties", arg_type=event_msg_prop_type) - - with self.argument_context("iot pnp") as context: - context.argument( - "model", - options_list=["--model", "-m"], - help="Target capability-model urn-id. Example: urn:example:capabilityModels:Mxchip:1", - ) - context.argument( - "interface", - options_list=["--interface", "-i"], - help="Target interface urn-id. Example: urn:example:interfaces:MXChip:1", - ) - - with self.argument_context("iot pnp interface") as context: - context.argument( - "interface_definition", - options_list=["--definition", "--def"], - help="IoT Plug and Play interface definition written in PPDL (JSON-LD). " - "Can be directly input or a file path where the content is extracted.", - ) - - with self.argument_context("iot pnp interface list") as context: - context.argument( - "search_string", - options_list=["--search", "--ss"], - help="Searches IoT Plug and Play interfaces for given string in the" - ' "Description, DisplayName, comment and Id".', - ) - context.argument( - "top", - type=int, - options_list=["--top"], - help="Maximum number of interface to return.", - ) - - with self.argument_context("iot pnp capability-model") as context: - context.argument( - "model_definition", - options_list=["--definition", "--def"], - help="IoT Plug and Play capability-model definition written in PPDL (JSON-LD). " - "Can be directly input or a file path where the content is extracted.", - ) - - with self.argument_context("iot pnp capability-model show") as context: - context.argument( - "expand", - options_list=["--expand"], - help="Indicates whether to expand the device capability model's" - " interface definitions or not.", - ) - - with self.argument_context("iot pnp capability-model list") as context: - context.argument( - "search_string", - options_list=["--search", "--ss"], - help="Searches IoT Plug and Play models for given string in the" - ' "Description, DisplayName, comment and Id".', - ) - context.argument( - "top", - type=int, - options_list=["--top"], - help="Maximum number of capability-model to return.", - ) diff --git a/azext_iot/_validators.py b/azext_iot/_validators.py index 8538c5538..822726a3c 100644 --- a/azext_iot/_validators.py +++ b/azext_iot/_validators.py @@ -23,9 +23,6 @@ def mode2_iot_login_handler(cmd, namespace): elif 'dps_name' in args: iot_cmd_type = 'DPS' entity_value = args['dps_name'] - elif 'repo_endpoint' in args: - iot_cmd_type = 'PnP' - entity_value = args['repo_endpoint'] if not any([login_value, entity_value]): raise CLIError(error_no_hub_or_login_on_input(iot_cmd_type)) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index c68ac9b36..404c3f228 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -11,8 +11,6 @@ from azext_iot import ( iothub_ops, iotdps_ops, - iotdigitaltwin_ops, - iotpnp_ops, ) @@ -162,33 +160,3 @@ def load_command_table(self, _): cmd_group.command("list", "iot_dps_registration_list") cmd_group.command("show", "iot_dps_registration_get") cmd_group.command("delete", "iot_dps_registration_delete") - - with self.command_group( - "iot dt", command_type=iotdigitaltwin_ops, is_preview=True - ) as cmd_group: - cmd_group.command("list-interfaces", "iot_digitaltwin_interface_list") - cmd_group.command("list-properties", "iot_digitaltwin_properties_list") - cmd_group.command("update-property", "iot_digitaltwin_property_update") - cmd_group.command("invoke-command", "iot_digitaltwin_invoke_command") - cmd_group.command("monitor-events", "iot_digitaltwin_monitor_events") - cmd_group.command("list-commands", "iot_digitaltwin_command_list") - - with self.command_group( - "iot pnp interface", command_type=iotpnp_ops, is_preview=True - ) as cmd_group: - cmd_group.command("show", "iot_pnp_interface_show") - cmd_group.command("list", "iot_pnp_interface_list") - cmd_group.command("create", "iot_pnp_interface_create") - cmd_group.command("publish", "iot_pnp_interface_publish") - cmd_group.command("delete", "iot_pnp_interface_delete") - cmd_group.command("update", "iot_pnp_interface_update") - - with self.command_group( - "iot pnp capability-model", command_type=iotpnp_ops, is_preview=True - ) as cmd_group: - cmd_group.command("show", "iot_pnp_model_show") - cmd_group.command("list", "iot_pnp_model_list") - cmd_group.command("create", "iot_pnp_model_create") - cmd_group.command("publish", "iot_pnp_model_publish") - cmd_group.command("delete", "iot_pnp_model_delete") - cmd_group.command("update", "iot_pnp_model_update") diff --git a/azext_iot/common/_azure.py b/azext_iot/common/_azure.py index 781f15534..c759095f4 100644 --- a/azext_iot/common/_azure.py +++ b/azext_iot/common/_azure.py @@ -154,92 +154,3 @@ def get_iot_central_tokens(cmd, app_id, central_dns_suffix): raise CLIError(error_message) return tokens - - -def get_iot_pnp_connection_string( - cmd, endpoint, repo_id, user_role="Admin", login=None -): - """ - Function used to build up dictionary of IoT PnP connection string parts - - Args: - cmd (object): Knack cmd - endpoint (str): PnP endpoint - repository_id (str): PnP repository Id. - user_role (str): User role of the access key for the given PnP repository. - - Returns: - (dict): of connection string elements. - - Raises: - CLIError: on input validation failure. - - """ - - from azure.cli.command_modules.iot.digitaltwinrepositoryprovisioningservice import ( - DigitalTwinRepositoryProvisioningService, - ) - from azure.cli.command_modules.iot._utils import get_auth_header - from azext_iot.constants import PNP_REPO_ENDPOINT - - result = {} - client = None - headers = None - - if login: - - try: - decomposed = parse_pnp_connection_string(login) - except ValueError as e: - raise CLIError(e) - - result = {} - result["cs"] = login - result["policy"] = decomposed["SharedAccessKeyName"] - result["primarykey"] = decomposed["SharedAccessKey"] - result["repository_id"] = decomposed["RepositoryId"] - result["entity"] = decomposed["HostName"] - result["entity"] = result["entity"].replace("https://", "") - result["entity"] = result["entity"].replace("http://", "") - return result - - def _find_key_from_list(keys, user_role): - if keys: - return next( - (key for key in keys if key.user_role.lower() == user_role.lower()), - None, - ) - return None - - if repo_id: - client = DigitalTwinRepositoryProvisioningService(endpoint) - headers = get_auth_header(cmd) - keys = client.get_keys_async( - repository_id=repo_id, - api_version=client.api_version, - custom_headers=headers, - ) - - if keys is None: - raise CLIError('Auth key required for repository "{}"'.format(repo_id)) - - policy = _find_key_from_list(keys, user_role) - - if policy is None: - raise CLIError( - 'No auth key found for repository "{}" with user_role "{}".'.format( - repo_id, user_role - ) - ) - - result["cs"] = policy.connection_string - result["entity"] = policy.service_endpoint - result["policy"] = policy.id - result["primarykey"] = policy.secret - result["repository_id"] = policy.repository_id - else: - result["entity"] = PNP_REPO_ENDPOINT - - result["entity"] = result["entity"].replace("https://", "") - result["entity"] = result["entity"].replace("http://", "") - return result diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 6d69f9432..f666c5bdd 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -34,10 +34,10 @@ MIN_SIM_MSG_INTERVAL = 1 MIN_SIM_MSG_COUNT = 1 SIM_RECEIVE_SLEEP_SEC = 3 -PNP_API_VERSION = "2019-07-01-preview" CENTRAL_ENDPOINT = "azureiotcentral.com" -PNP_ENDPOINT = "https://provider.azureiotrepository.com" -PNP_REPO_ENDPOINT = "https://repo.azureiotrepository.com" +PNP_API_VERSION = "2020-05-01-preview" +PNP_ENDPOINT = "azureiotrepository.com" +PNP_TENANT_RESOURCE_ID = "822c8694-ad95-4735-9c55-256f7db2f9b4" DEVICE_DEVICESCOPE_PREFIX = "ms-azure-iot-edge://" TRACING_PROPERTY = "azureiot*com^dtracing^1" TRACING_ALLOWED_FOR_LOCATION = ("northeurope", "westus2", "west us 2", "southeastasia") diff --git a/azext_iot/iothub/_help.py b/azext_iot/iothub/_help.py index c47362dbb..77934f1d4 100644 --- a/azext_iot/iothub/_help.py +++ b/azext_iot/iothub/_help.py @@ -17,7 +17,7 @@ def load_iothub_help(): short-summary: Manage IoT Hub jobs (v2). """ - helps['iot hub job create'] = """ + helps["iot hub job create"] = """ type: command short-summary: Create and schedule an IoT Hub job for execution. long-summary: | @@ -45,7 +45,7 @@ def load_iothub_help(): --query-condition "properties.reported.settings.syncIntervalSec != 30" """ - helps['iot hub job show'] = """ + helps["iot hub job show"] = """ type: command short-summary: Show details of an existing IoT Hub job. @@ -55,7 +55,7 @@ def load_iothub_help(): az iot hub job show --hub-name {iothub_name} --job-id {job_id} """ - helps['iot hub job list'] = """ + helps["iot hub job list"] = """ type: command short-summary: List the historical jobs of an IoT Hub. @@ -77,7 +77,7 @@ def load_iothub_help(): az iot hub job list --hub-name {iothub_name} --job-type export --job-status completed """ - helps['iot hub job cancel'] = """ + helps["iot hub job cancel"] = """ type: command short-summary: Cancel an IoT Hub job. @@ -86,3 +86,57 @@ def load_iothub_help(): text: > az iot hub job cancel --hub-name {iothub_name} --job-id {job_id} """ + + helps["iot pnp twin"] = """ + type: group + short-summary: Manipulate and interact with the digital twin of an IoT Hub device. + """ + + helps["iot pnp twin invoke-command"] = """ + type: command + short-summary: Invoke a root or component level command of a digital twin device. + + examples: + - name: Invoke root level command "reboot" which takes a payload that includes the "delay" property. + text: > + az iot pnp twin invoke-command --command-name reboot -n {iothub_name} -d {device_id} --payload '{"delay":5}' + + - name: Invoke command "getMaxMinReport" on component "thermostat1" that takes no input. + text: > + az iot pnp twin invoke-command --cn getMaxMinReport -n {iothub_name} -d {device_id} --component-path thermostat1 + """ + + helps["iot pnp twin show"] = """ + type: command + short-summary: Show the digital twin of an IoT Hub device. + + examples: + - name: Show the target device digital twin. + text: > + az iot pnp twin show -n {iothub_name} -d {device_id} + """ + + helps["iot pnp twin update"] = """ + type: command + short-summary: Update the read-write properties of a digital twin device via JSON patch specification. + long-summary: Currently operations are limited to add, replace and remove. + + examples: + - name: Update a digital twin via JSON patch specification. + text: > + az iot pnp twin update --hub-name {iothub_name} --device-id {device_id} + --json-patch '{"op":"add", "path":"/thermostat1/targetTemperature", "value": 54}' + + - name: Update a digital twin via JSON patch specification. + text: > + az iot pnp twin update -n {iothub_name} -d {device_id} + --json-patch '[ + {"op":"remove", "path":"/thermostat1/targetTemperature"}, + {"op":"add", "path":"/thermostat2/targetTemperature", "value": 22} + ]' + + - name: Update a digital twin property via JSON patch specification defined in a file. + text: > + az iot pnp twin update -n {iothub_name} -d {device_id} + --json-patch ./my/patch/document.json + """ diff --git a/azext_iot/iothub/command_bindings.py b/azext_iot/iothub/command_map.py similarity index 51% rename from azext_iot/iothub/command_bindings.py rename to azext_iot/iothub/command_map.py index aeaba9e2a..a100d797e 100644 --- a/azext_iot/iothub/command_bindings.py +++ b/azext_iot/iothub/command_map.py @@ -7,19 +7,25 @@ """ Load CLI commands """ +from azure.cli.core.commands import CliCommandType -from azext_iot import iothub_ops_job, iothub_ops_device +pnp_runtime_ops = CliCommandType( + operations_tmpl="azext_iot.iothub.commands_pnp_runtime#{}" +) +iothub_job_ops = CliCommandType(operations_tmpl="azext_iot.iothub.commands_job#{}") def load_iothub_commands(self, _): """ Load CLI commands """ - with self.command_group("iot hub job", command_type=iothub_ops_job) as cmd_group: + with self.command_group("iot hub job", command_type=iothub_job_ops) as cmd_group: cmd_group.command("create", "job_create") cmd_group.command("show", "job_show") cmd_group.command("list", "job_list") cmd_group.command("cancel", "job_cancel") - with self.command_group("iot hub device-identity", command_type=iothub_ops_device) as cmd_group: - pass + with self.command_group("iot pnp twin", command_type=pnp_runtime_ops, is_preview=True) as cmd_group: + cmd_group.command("invoke-command", "invoke_device_command") + cmd_group.command("show", "get_digital_twin") + cmd_group.command("update", "patch_digital_twin") diff --git a/azext_iot/iothub/job_commands.py b/azext_iot/iothub/commands_job.py similarity index 100% rename from azext_iot/iothub/job_commands.py rename to azext_iot/iothub/commands_job.py diff --git a/azext_iot/iothub/commands_pnp_runtime.py b/azext_iot/iothub/commands_pnp_runtime.py new file mode 100644 index 000000000..eba7a2f70 --- /dev/null +++ b/azext_iot/iothub/commands_pnp_runtime.py @@ -0,0 +1,64 @@ +# 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.iothub.providers.pnp_runtime import PnPRuntimeProvider +from knack.log import get_logger + +logger = get_logger(__name__) + + +def invoke_device_command( + cmd, + device_id, + command_name, + timeout=30, + component_path=None, + payload="{}", + hub_name=None, + resource_group_name=None, + login=None, +): + runtime_provider = PnPRuntimeProvider( + cmd=cmd, hub_name=hub_name, rg=resource_group_name, login=login + ) + return runtime_provider.invoke_device_command( + device_id=device_id, + command_name=command_name, + payload=payload, + component_path=component_path, + timeout=timeout, + ) + + +def get_digital_twin( + cmd, + device_id, + hub_name=None, + resource_group_name=None, + login=None, +): + runtime_provider = PnPRuntimeProvider( + cmd=cmd, hub_name=hub_name, rg=resource_group_name, login=login + ) + return runtime_provider.get_digital_twin( + device_id=device_id, + ) + + +def patch_digital_twin( + cmd, + device_id, + json_patch, + hub_name=None, + resource_group_name=None, + login=None, +): + runtime_provider = PnPRuntimeProvider( + cmd=cmd, hub_name=hub_name, rg=resource_group_name, login=login + ) + return runtime_provider.patch_digital_twin( + device_id=device_id, json_patch=json_patch + ) diff --git a/azext_iot/iothub/device_commands.py b/azext_iot/iothub/device_commands.py deleted file mode 100644 index 68a53aa8d..000000000 --- a/azext_iot/iothub/device_commands.py +++ /dev/null @@ -1,16 +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 knack.log import get_logger -from azext_iot.iothub.providers.device_identity import DeviceIdentityProvider - - -logger = get_logger(__name__) - - -def get_device_metrics(cmd, hub_name=None, resource_group_name=None, login=None): - device_provider = DeviceIdentityProvider(cmd=cmd, hub_name=hub_name, rg=resource_group_name, login=login) - return device_provider.get_device_stats() diff --git a/azext_iot/iothub/params.py b/azext_iot/iothub/params.py new file mode 100644 index 000000000..f1beee189 --- /dev/null +++ b/azext_iot/iothub/params.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +def load_iothub_arguments(self, _): + """ + Load CLI Args for Knack parser + """ + with self.argument_context("iot pnp twin") as context: + context.argument( + "command_name", + options_list=["--command-name", "--cn"], + help="Digital twin command name.", + ) + context.argument( + "component_path", + options_list=["--component-path"], + help="Digital twin component path. For example: thermostat1.", + ) + context.argument( + "json_patch", + options_list=["--json-patch", "--patch"], + help="An update specification described by JSON-patch. " + "Operations are limited to add, replace and remove. Provide file path or inline JSON.", + ) + context.argument( + "payload", + options_list=["--payload"], + help="JSON payload input for command. Provide file path or inline JSON.", + ) diff --git a/azext_iot/iothub/providers/device_identity.py b/azext_iot/iothub/providers/device_identity.py deleted file mode 100644 index 1c982151a..000000000 --- a/azext_iot/iothub/providers/device_identity.py +++ /dev/null @@ -1,23 +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 knack.log import get_logger -from knack.util import CLIError -from azext_iot.common.shared import SdkType -from azext_iot.common.utility import unpack_msrest_error -from azext_iot.iothub.providers.base import IoTHubProvider, CloudError - - -logger = get_logger(__name__) - - -class DeviceIdentityProvider(IoTHubProvider): - def get_device_stats(self): - service_sdk = self.get_sdk(SdkType.service_sdk) - try: - return service_sdk.registry_manager.get_device_statistics() - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/iothub/providers/pnp_runtime.py b/azext_iot/iothub/providers/pnp_runtime.py new file mode 100644 index 000000000..f99bcc09e --- /dev/null +++ b/azext_iot/iothub/providers/pnp_runtime.py @@ -0,0 +1,101 @@ +# 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 knack.log import get_logger +from knack.util import CLIError +from azext_iot.common.shared import SdkType +from azext_iot.common.utility import unpack_msrest_error, process_json_arg +from azext_iot.iothub.providers.base import ( + IoTHubProvider, + CloudError, +) + + +logger = get_logger(__name__) + + +class PnPRuntimeProvider(IoTHubProvider): + def __init__(self, cmd, hub_name=None, rg=None, login=None): + super(PnPRuntimeProvider, self).__init__( + cmd=cmd, hub_name=hub_name, rg=rg, login=login + ) + self.runtime_sdk = self.get_sdk(SdkType.pnp_sdk).digital_twin + + def invoke_device_command( + self, + device_id, + command_name, + timeout=30, + payload="{}", + component_path=None, + connect_timeout=None, + response_timeout=None, + ): + + # Prevent msrest locking up shell + self.runtime_sdk.config.retry_policy.retries = 1 + + try: + if payload: + payload = process_json_arg(payload, argument_name="payload") + + api_timeout_kwargs = { + "connect_timeout_in_seconds": timeout, + "response_timeout_in_seconds": timeout, + } + response = ( + self.runtime_sdk.invoke_component_command( + id=device_id, + command_name=command_name, + payload=payload, + timeout=timeout, + component_path=component_path, + raw=True, + **api_timeout_kwargs, + ).response + if component_path + else self.runtime_sdk.invoke_root_level_command( + id=device_id, + command_name=command_name, + payload=payload, + timeout=timeout, + raw=True, + **api_timeout_kwargs, + ).response + ) + + return { + "payload": response.json(), + "status": response.headers.get("x-ms-command-statuscode"), + } + + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def get_digital_twin(self, device_id): + return self.runtime_sdk.get_digital_twin(id=device_id, raw=True).response.json() + + def patch_digital_twin(self, device_id, json_patch): + json_patch = process_json_arg(content=json_patch, argument_name="json-patch") + + json_patch_collection = [] + if isinstance(json_patch, dict): + json_patch_collection.append(json_patch) + if isinstance(json_patch, list): + json_patch_collection.extend(json_patch) + + logger.info("Patch payload %s", json.dumps(json_patch_collection)) + + try: + # Currently no response text is returned from the update + self.runtime_sdk.update_digital_twin( + id=device_id, digital_twin_patch=json_patch_collection, if_match="*", raw=True + ).response + + return self.get_digital_twin(device_id=device_id) + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/monitor/parsers/common_parser.py b/azext_iot/monitor/parsers/common_parser.py index da64b359e..1ddc22e9c 100644 --- a/azext_iot/monitor/parsers/common_parser.py +++ b/azext_iot/monitor/parsers/common_parser.py @@ -17,8 +17,10 @@ from azext_iot.monitor.parsers.issue import IssueHandler DEVICE_ID_IDENTIFIER = b"iothub-connection-device-id" -INTERFACE_NAME_IDENTIFIER = b"iothub-interface-name" MODULE_ID_IDENTIFIER = b"iothub-connection-module-id" +INTERFACE_NAME_IDENTIFIER_V1 = b"iothub-interface-name" +INTERFACE_NAME_IDENTIFIER_V2 = b"dt-dataschema" +COMPONENT_NAME_IDENTIFIER = b"dt-subject" class CommonParser(AbstractBaseParser): @@ -28,18 +30,25 @@ def __init__(self, message: Message, common_parser_args: CommonParserArguments): self._message = message self.device_id = "" # need to default self.device_id = self._parse_device_id(message) - self.interface_name = self._parse_interface_name(message) self.module_id = self._parse_module_id(message) + self.interface_name = self._parse_interface_name(message) + self.component_name = self._parse_component_name(message) def parse_message(self) -> dict: + """ + Parses an AMQP based IoT Hub telemetry event. + + """ + message = self._message properties = self._common_parser_args.properties content_type = self._common_parser_args.content_type event = {} event["origin"] = self.device_id - event["interface"] = self.interface_name event["module"] = self.module_id + event["interface"] = self.interface_name + event["component"] = self.component_name if not properties: properties = [] # guard against None being passed in @@ -98,12 +107,24 @@ def _parse_module_id(self, message: Message) -> str: def _parse_interface_name(self, message: Message) -> str: try: - return str(message.annotations.get(INTERFACE_NAME_IDENTIFIER), "utf8") + # Grab either the DTDL v1 or v2 amqp interface identifier. + # It's highly unlikely both will be present at the same time + # as they reflect different versions of a Plug & Play device. + target_interface = message.annotations.get( + INTERFACE_NAME_IDENTIFIER_V1 + ) or message.annotations.get(INTERFACE_NAME_IDENTIFIER_V2) + return str(target_interface, "utf8") except Exception: # a message not containing an interface name is expected for non-pnp devices # so there's no "issue" to log here return "" + def _parse_component_name(self, message: Message) -> str: + try: + return str(message.annotations.get(COMPONENT_NAME_IDENTIFIER), "utf8") + except Exception: + return "" + def _parse_system_properties(self, message: Message): try: return unicode_binary_map(parse_entity(message.properties, True)) diff --git a/azext_iot/operations/digitaltwin.py b/azext_iot/operations/digitaltwin.py deleted file mode 100644 index 1156237fb..000000000 --- a/azext_iot/operations/digitaltwin.py +++ /dev/null @@ -1,389 +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 os.path import exists -from knack.util import CLIError -from azext_iot.constants import PNP_ENDPOINT -from azext_iot._factory import _bind_sdk -from azext_iot.common.shared import SdkType, ModelSourceType -from azext_iot.iothub.providers.discovery import IotHubDiscovery -from azext_iot.common.utility import ( - shell_safe_json_parse, - read_file_content, - unpack_msrest_error, -) -from azext_iot.operations.pnp import ( - iot_pnp_interface_show, - iot_pnp_interface_list, - _validate_repository, -) -from azext_iot.operations.hub import _iot_hub_monitor_events - - -INTERFACE_KEY_NAME = "urn_azureiot_ModelDiscovery_DigitalTwin" -INTERFACE_COMMAND = "Command" -INTERFACE_PROPERTY = "Property" -INTERFACE_TELEMETRY = "Telemetry" -INTERFACE_MODELDEFINITION = "urn_azureiot_ModelDiscovery_ModelDefinition" -INTERFACE_COMMANDNAME = "getModelDefinition" - - -def iot_digitaltwin_interface_list( - cmd, device_id, hub_name=None, resource_group_name=None, login=None -): - device_default_interface = _iot_digitaltwin_interface_show( - cmd, device_id, INTERFACE_KEY_NAME, hub_name, resource_group_name, login - ) - result = _get_device_default_interface_dict(device_default_interface) - return {"interfaces": result} - - -def iot_digitaltwin_command_list( - cmd, - device_id, - source_model, - interface=None, - schema=False, - repo_endpoint=PNP_ENDPOINT, - repo_id=None, - repo_login=None, - hub_name=None, - resource_group_name=None, - login=None, -): - result = [] - target_interfaces = [] - source_model = source_model.lower() - device_interfaces = _iot_digitaltwin_interface_list( - cmd, device_id, hub_name, resource_group_name, login - ) - interface_list = _get_device_default_interface_dict(device_interfaces) - target_interface = next( - (item for item in interface_list if item["name"] == interface), None - ) - if interface and not target_interface: - raise CLIError("Target interface is not implemented by the device!") - - if interface: - target_interfaces.append(target_interface) - else: - target_interfaces = interface_list - - for entity in target_interfaces: - interface_result = { - "name": entity["name"], - "urn_id": entity["urn_id"], - "commands": {}, - } - interface_commands = [] - found_commands = [] - if source_model == ModelSourceType.device.value.lower(): - found_commands = _device_interface_elements( - cmd, - device_id, - entity["urn_id"], - INTERFACE_COMMAND, - hub_name, - resource_group_name, - login, - ) - else: - if source_model == ModelSourceType.private.value.lower(): - _validate_repository(repo_id, repo_login) - found_commands = _pnp_interface_elements( - cmd, - entity["urn_id"], - INTERFACE_COMMAND, - repo_endpoint, - repo_id, - repo_login, - ) - for command in found_commands: - command.pop("@type", None) - if schema: - interface_commands.append(command) - else: - interface_commands.append(command.get("name")) - interface_result["commands"] = interface_commands - result.append(interface_result) - return {"interfaces": result} - - -def iot_digitaltwin_properties_list( - cmd, - device_id, - source_model, - interface=None, - schema=False, - repo_endpoint=PNP_ENDPOINT, - repo_id=None, - repo_login=None, - hub_name=None, - resource_group_name=None, - login=None, -): - result = [] - target_interfaces = [] - source_model = source_model.lower() - device_interfaces = _iot_digitaltwin_interface_list( - cmd, device_id, hub_name, resource_group_name, login - ) - interface_list = _get_device_default_interface_dict(device_interfaces) - target_interface = next( - (item for item in interface_list if item["name"] == interface), None - ) - if interface and not target_interface: - raise CLIError("Target interface is not implemented by the device!") - - if interface: - target_interfaces.append(target_interface) - else: - target_interfaces = interface_list - - for entity in target_interfaces: - interface_result = { - "name": entity["name"], - "urn_id": entity["urn_id"], - "properties": {}, - } - interface_properties = [] - found_properties = [] - if source_model == ModelSourceType.device.value.lower(): - found_properties = _device_interface_elements( - cmd, - device_id, - entity["urn_id"], - INTERFACE_PROPERTY, - hub_name, - resource_group_name, - login, - ) - else: - if source_model == ModelSourceType.private.value.lower(): - _validate_repository(repo_id, repo_login) - found_properties = _pnp_interface_elements( - cmd, - entity["urn_id"], - INTERFACE_PROPERTY, - repo_endpoint, - repo_id, - repo_login, - ) - for prop in found_properties: - prop.pop("@type", None) - if schema: - interface_properties.append(prop) - else: - interface_properties.append(prop.get("name")) - interface_result["properties"] = interface_properties - result.append(interface_result) - return {"interfaces": result} - - -def iot_digitaltwin_invoke_command( - cmd, - interface, - device_id, - command_name, - command_payload=None, - timeout=10, - hub_name=None, - resource_group_name=None, - login=None, -): - device_interfaces = _iot_digitaltwin_interface_list( - cmd, device_id, hub_name, resource_group_name, login - ) - interface_list = _get_device_default_interface_dict(device_interfaces) - - target_interface = next( - (item for item in interface_list if item["name"] == interface), None - ) - - if not target_interface: - raise CLIError("Target interface is not implemented by the device!") - - if command_payload: - if exists(command_payload): - command_payload = str(read_file_content(command_payload)) - - target_json = None - try: - target_json = shell_safe_json_parse(command_payload) - except ValueError: - pass - - if target_json or isinstance(target_json, bool): - command_payload = target_json - - discovery = IotHubDiscovery(cmd) - target = discovery.get_target(hub_name=hub_name, resource_group_name=resource_group_name, login=login) - service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) - try: - result = service_sdk.invoke_interface_command( - device_id, - interface, - command_name, - command_payload, - connect_timeout_in_seconds=timeout, - response_timeout_in_seconds=timeout, - ) - return result - except errors.CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - -def iot_digitaltwin_property_update( - cmd, - interface_payload, - device_id, - hub_name=None, - resource_group_name=None, - login=None, -): - if exists(interface_payload): - interface_payload = str(read_file_content(interface_payload)) - - target_json = None - try: - target_json = shell_safe_json_parse(interface_payload) - except ValueError: - pass - - if target_json: - interface_payload = target_json - - discovery = IotHubDiscovery(cmd) - target = discovery.get_target(hub_name=hub_name, resource_group_name=resource_group_name, login=login) - service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) - try: - result = service_sdk.update_interfaces(device_id, interfaces=interface_payload) - return result - except errors.CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - -def iot_digitaltwin_monitor_events( - cmd, - hub_name=None, - device_id=None, - consumer_group="$Default", - timeout=300, - enqueued_time=None, - resource_group_name=None, - yes=False, - properties=None, - repair=False, - login=None, - content_type=None, - device_query=None, - interface=None, - module_id=None, -): - _iot_hub_monitor_events( - cmd=cmd, - hub_name=hub_name, - device_id=device_id, - consumer_group=consumer_group, - timeout=timeout, - enqueued_time=enqueued_time, - resource_group_name=resource_group_name, - yes=yes, - properties=properties, - repair=repair, - login=login, - content_type=content_type, - device_query=device_query, - interface_name=interface, - module_id=module_id, - ) - - -def _iot_digitaltwin_interface_show( - cmd, device_id, interface, 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) - service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) - try: - device_interface = service_sdk.get_interface(device_id, interface) - return device_interface - except errors.CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - -def _iot_digitaltwin_interface_list( - cmd, device_id, hub_name=None, resource_group_name=None, login=None -): - discovery = IotHubDiscovery(cmd) - target = discovery.get_target(hub_name=hub_name, resource_group_name=resource_group_name, login=login) - service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) - try: - device_interfaces = service_sdk.get_interfaces(device_id) - return device_interfaces - except errors.CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - -def _get_device_default_interface_dict(device_default_interface): - interface = device_default_interface["interfaces"][INTERFACE_KEY_NAME] - result = [] - for k, v in interface["properties"]["modelInformation"]["reported"]["value"][ - "interfaces" - ].items(): - result.append({"name": k, "urn_id": v}) - return result - - -def _pnp_interface_elements(cmd, interface, target_type, repo_endpoint, repo_id, login): - interface_elements = [] - results = iot_pnp_interface_list( - cmd, repo_endpoint, repo_id, interface, login=login - ) - if results: - interface_def = iot_pnp_interface_show( - cmd, interface, repo_endpoint, repo_id, login - ) - interface_contents = interface_def.get("contents") - for content in interface_contents: - if isinstance(content.get("@type"), list) and target_type in content.get( - "@type" - ): - interface_elements.append(content) - elif content.get("@type") == target_type: - interface_elements.append(content) - return interface_elements - - -def _device_interface_elements( - cmd, device_id, interface, target_type, hub_name, resource_group_name, login -): - discovery = IotHubDiscovery(cmd) - target = discovery.get_target(hub_name=hub_name, resource_group_name=resource_group_name, login=login) - service_sdk, errors = _bind_sdk(target, SdkType.service_sdk) - interface_elements = [] - try: - payload = {"id": {}} - payload["id"] = interface - target_payload = shell_safe_json_parse(str(payload)) - interface_def = service_sdk.invoke_interface_command( - device_id, INTERFACE_MODELDEFINITION, INTERFACE_COMMANDNAME, target_payload - ) - if interface_def and interface_def.get("contents"): - interface_contents = interface_def.get("contents") - for content in interface_contents: - if isinstance( - content.get("@type"), list - ) and target_type in content.get("@type"): - interface_elements.append(content) - elif content.get("@type") == target_type: - interface_elements.append(content) - return interface_elements - except errors.CloudError as e: - raise CLIError(unpack_msrest_error(e)) - except Exception: - # returning an empty collection to continue - return [] diff --git a/azext_iot/operations/pnp.py b/azext_iot/operations/pnp.py deleted file mode 100644 index ed6d3be13..000000000 --- a/azext_iot/operations/pnp.py +++ /dev/null @@ -1,234 +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. -# -------------------------------------------------------------------------------------------- - -import json -from os.path import exists -from knack.log import get_logger -from knack.util import CLIError -from azext_iot.common.shared import SdkType, PnPModelType -from azext_iot.common._azure import get_iot_pnp_connection_string -from azext_iot.sdk.pnp.models import SearchOptions -from azext_iot._factory import _bind_sdk -from azext_iot.constants import PNP_API_VERSION, PNP_ENDPOINT -from azext_iot.common.utility import (unpack_pnp_http_error, - get_sas_token, - shell_safe_json_parse, - read_file_content, - looks_like_file) - -logger = get_logger(__name__) - - -def iot_pnp_interface_publish(cmd, interface, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): - _validate_repository(repo_id, login) - model_list = _iot_pnp_model_list(cmd, repo_endpoint, repo_id, interface, PnPModelType.interface, - -1, login=login) - if model_list and model_list[0].urn_id == interface: - etag = model_list[0].etag - else: - raise CLIError('No PnP Model definition found for @id "{}"'.format(interface)) - - target_interface = _iot_pnp_model_show(cmd, repo_endpoint, repo_id, - interface, False, PnPModelType.interface, login=login) - - return _iot_pnp_model_publish(cmd, repo_endpoint, repo_id, interface, target_interface, - etag, login=login) - - -def iot_pnp_interface_create(cmd, interface_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): - _validate_repository(repo_id, login) - return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, interface_definition, - PnPModelType.interface, False, login=login) - - -def iot_pnp_interface_update(cmd, interface_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): - _validate_repository(repo_id, login) - return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, interface_definition, - PnPModelType.interface, True, login=login) - - -def iot_pnp_interface_show(cmd, interface, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): - return _iot_pnp_model_show(cmd, repo_endpoint, repo_id, - interface, False, PnPModelType.interface, login=login) - - -def iot_pnp_interface_list(cmd, repo_endpoint=PNP_ENDPOINT, repo_id=None, search_string=None, - top=1000, login=None): - return _iot_pnp_model_list(cmd, repo_endpoint, repo_id, - search_string, PnPModelType.interface, - top, login=login) - - -def iot_pnp_interface_delete(cmd, interface, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): - _validate_repository(repo_id, login) - return _iot_pnp_model_delete(cmd, repo_endpoint, repo_id, interface, login) - - -def iot_pnp_model_publish(cmd, model, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): - _validate_repository(repo_id, login) - model_list = _iot_pnp_model_list(cmd, repo_endpoint, repo_id, model, PnPModelType.capabilityModel, - -1, login=login) - if model_list and model_list[0].urn_id == model: - etag = model_list[0].etag - else: - raise CLIError('No PnP Model definition found for @id "{}"'.format(model)) - - target_model = _iot_pnp_model_show(cmd, repo_endpoint, repo_id, - model, False, PnPModelType.capabilityModel, login=login) - return _iot_pnp_model_publish(cmd, repo_endpoint, repo_id, model, target_model, - etag, login=login) - - -def iot_pnp_model_create(cmd, model_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): - _validate_repository(repo_id, login) - return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, model_definition, - PnPModelType.capabilityModel, False, login=login) - - -def iot_pnp_model_update(cmd, model_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): - _validate_repository(repo_id, login) - return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, model_definition, - PnPModelType.capabilityModel, True, login=login) - - -def iot_pnp_model_show(cmd, model, repo_endpoint=PNP_ENDPOINT, repo_id=None, expand=False, login=None): - return _iot_pnp_model_show(cmd, repo_endpoint, repo_id, - model, expand, PnPModelType.capabilityModel, login=login) - - -def iot_pnp_model_list(cmd, repo_endpoint=PNP_ENDPOINT, repo_id=None, search_string=None, - top=1000, login=None): - return _iot_pnp_model_list(cmd, repo_endpoint, repo_id, - search_string, PnPModelType.capabilityModel, - top, login=login) - - -def iot_pnp_model_delete(cmd, model, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None): - _validate_repository(repo_id, login) - return _iot_pnp_model_delete(cmd, repo_endpoint, repo_id, model, login) - - -def _iot_pnp_model_publish(cmd, endpoint, repository, model_id, model_def, etag, login): - - target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) - pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) - - contents = json.loads(json.dumps(model_def, separators=(',', ':'), indent=2)) - try: - headers = get_sas_token(target) - return pnp_sdk.create_or_update_model(model_id, - api_version=PNP_API_VERSION, - content=contents, - if_match=etag, - custom_headers=headers) - except errors.HttpOperationError as e: - raise CLIError(unpack_pnp_http_error(e)) - - -def _iot_pnp_model_create_or_update(cmd, endpoint, repository, model_def, pnpModelType, is_update, login): - - target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) - pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) - etag = None - model_def = _validate_model_definition(model_def) - model_id = model_def.get('@id') - if not model_id: - raise CLIError('PnP Model definition requires @id! Please include @id and try again.') - - if is_update: - model_list = _iot_pnp_model_list(cmd, endpoint, repository, model_id, pnpModelType, - -1, login=login) - if model_list and model_list[0].urn_id == model_id: - etag = model_list[0].etag - else: - raise CLIError('No PnP Model definition found for @id "{}"'.format(model_id)) - - contents = json.loads(json.dumps(model_def, separators=(',', ':'), indent=2)) - try: - headers = get_sas_token(target) - return pnp_sdk.create_or_update_model(model_id, - api_version=PNP_API_VERSION, - content=contents, - repository_id=target.get('repository_id', None), - if_match=etag, - custom_headers=headers) - except errors.HttpOperationError as e: - raise CLIError(unpack_pnp_http_error(e)) - - -def _iot_pnp_model_show(cmd, endpoint, repository, model_id, expand, pnpModelType, login): - target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) - pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) - try: - headers = get_sas_token(target) - result = pnp_sdk.get_model(model_id, api_version=PNP_API_VERSION, - repository_id=target.get('repository_id', None), - custom_headers=headers, - expand=expand) - - if not result or result["@type"].lower() != pnpModelType.value.lower(): - raise CLIError('PnP Model definition for "{}", not found.'.format(model_id)) - - return result - except errors.HttpOperationError as e: - raise CLIError(unpack_pnp_http_error(e)) - - -def _iot_pnp_model_list(cmd, endpoint, repository, search_string, - pnpModelType, top, login): - target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) - - pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) - try: - headers = get_sas_token(target) - search_options = SearchOptions(search_keyword=search_string, - model_filter_type=pnpModelType.value) - if top > 0: - search_options.page_size = top - - result = pnp_sdk.search(search_options, api_version=PNP_API_VERSION, - repository_id=target.get('repository_id', None), - custom_headers=headers) - return result.results - except errors.HttpOperationError as e: - raise CLIError(unpack_pnp_http_error(e)) - - -def _iot_pnp_model_delete(cmd, endpoint, repository, model_id, login): - target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login) - - pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk) - try: - headers = get_sas_token(target) - return pnp_sdk.delete_model(model_id, - repository_id=target.get('repository_id', None), - api_version=PNP_API_VERSION, - custom_headers=headers) - except errors.HttpOperationError as e: - raise CLIError(unpack_pnp_http_error(e)) - - -def _validate_model_definition(model_def): - if exists(model_def): - model_def = str(read_file_content(model_def)) - else: - logger.info('Definition not from file path or incorrect path given.') - - try: - return shell_safe_json_parse(model_def) - except ValueError as e: - logger.debug('Received definition: %s', model_def) - if looks_like_file(model_def): - raise CLIError('The definition content looks like its from a file. Please ensure the path is correct.') - raise CLIError('Malformed capability model definition. ' - 'Use --debug to see what was received. Error details: {}'.format(e)) - - -def _validate_repository(repo_id, login): - if not login and not repo_id: - raise CLIError('Please provide the model repository\'s repositoryId (via the \'--repo-id\' or \'-r\' parameter)' - ' and endpoint (via the \'--endpoint\' or \'-e\' parameter) or model repository\'s connection' - ' string via --login...') diff --git a/azext_iot/pnp/__init__.py b/azext_iot/pnp/__init__.py new file mode 100644 index 000000000..8bec87150 --- /dev/null +++ b/azext_iot/pnp/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from ._help import load_pnp_help + +load_pnp_help() diff --git a/azext_iot/pnp/_help.py b/azext_iot/pnp/_help.py new file mode 100644 index 000000000..2b802af8a --- /dev/null +++ b/azext_iot/pnp/_help.py @@ -0,0 +1,145 @@ +# 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. +# -------------------------------------------------------------------------------------------- +""" +Help definitions for PnP commands +""" + +from knack.help_files import helps + + +def load_pnp_help(): + + helps["iot pnp"] = """ + type: group + short-summary: Manage Azure IoT Plug-and-Play repositories and models. + """ + + helps["iot pnp repo"] = """ + type: group + short-summary: Create and view Azure IoT Plug-and-Play tenant repositories. + """ + + helps["iot pnp repo create"] = """ + type: command + short-summary: Create a new PnP company repository for your tenant. + long-summary: | + Note that this command takes no parameters. The company repository will be created in your tenant, + and the user who creates the repository will be granted the TenantAdministrator role. + """ + + helps["iot pnp repo list"] = """ + type: command + short-summary: List PnP repositories for your tenant + """ + + helps["iot pnp role-assignment"] = """ + type: group + short-summary: Manage and configure PnP repository and model role assignments. + """ + + helps["iot pnp role-assignment list"] = """ + type: command + short-summary: Lists role assignments for a specific tenant or model. Can be filtered by subject-id. + + examples: + - name: List role assignments for a specific tenant repository + text: > + az iot pnp role-assignment list --resource-id {tenant_id} --resource-type Tenant + + - name: List role assignments for a specific model and subject. + text: > + az iot pnp role-assignment list --resource-id {model_id} + --resource-type Model + --subject-id {user_or_spn_id} + """ + + helps["iot pnp role-assignment create"] = """ + type: command + short-summary: Creates a role assignment for a user or service principal to a specific resource. + + examples: + - name: Assign a user the role of Tenant Administrator + text: > + az iot pnp role-assignment create --resource-id {tenant_id} + --resource-type Tenant + --role TenantAdministrator + --subject-id {user_id} + --subject-type User + + - name: Assign a service principal the role of Model Administrator + text: > + az iot pnp role-assignment create --resource-id {tenant_id} + --resource-type Tenant + --role ModelAdministrator + --subject-id {spn_id} + --subject-type ServicePrincipal + """ + + helps["iot pnp role-assignment delete"] = """ + type: command + short-summary: Deletes a role assignment for a user or service principal to a specific resource + + examples: + - name: Remove an assigned role for a specific user + text: > + az iot pnp role-assignment delete --resource-id {tenant_id} + --resource-type Tenant + --role {role} + --subject-id {user_id} + """ + + helps["iot pnp model"] = """ + type: group + short-summary: Create, view, and publish device models in your company repository. + """ + + helps["iot pnp model create"] = """ + type: command + short-summary: Create a new device model in your company repository + + examples: + - name: Create a new model by uploading a JSON file + text: > + az iot pnp model create --model {path\\to\\definition\\file.json} + """ + + helps["iot pnp model show"] = """ + type: command + short-summary: View a device model by ID + + examples: + - name: View a model with the ID {dtmi:my:model} + text: > + az iot pnp model show --dtmi {dtmi:my:model} + """ + + helps["iot pnp model list"] = """ + type: command + short-summary: List or search for models in the PnP model repository + + examples: + - name: List all models in the repository + text: > + az iot pnp model list + + - name: Search for all 'Listed' models created by a specific user or spn + text: > + az iot pnp model list --state Listed --created-by {user_or_spn_id} + + - name: Search for shared interfaces with name or description matching `{keyword}` + text: > + az iot pnp model list -q {keyword} --shared --type Interface + """ + + helps["iot pnp model publish"] = """ + type: command + short-summary: Publish a device model located in your company repository. + + examples: + - name: Publish a model with the ID {dtmi:my:model} + text: > + az iot pnp model publish --model-id {dtmi:my:model} + """ diff --git a/azext_iot/pnp/command_map.py b/azext_iot/pnp/command_map.py new file mode 100644 index 000000000..c35ad94c8 --- /dev/null +++ b/azext_iot/pnp/command_map.py @@ -0,0 +1,39 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +""" +Load CLI commands +""" +from azure.cli.core.commands import CliCommandType + +pnp_repo_ops = CliCommandType(operations_tmpl="azext_iot.pnp.commands_repository#{}") +pnp_model_ops = CliCommandType(operations_tmpl="azext_iot.pnp.commands_api#{}") + + +def load_pnp_commands(self, _): + """ + Load CLI commands + """ + with self.command_group( + "iot pnp role-assignment", command_type=pnp_repo_ops, is_preview=True + ) as cmd_group: + cmd_group.command("create", "iot_pnp_role_create") + cmd_group.command("list", "iot_pnp_role_list") + cmd_group.command("delete", "iot_pnp_role_delete") + + with self.command_group( + "iot pnp repo", command_type=pnp_repo_ops, is_preview=True + ) as cmd_group: + cmd_group.command("create", "iot_pnp_tenant_create") + cmd_group.command("list", "iot_pnp_tenant_show") + + with self.command_group( + "iot pnp model", command_type=pnp_model_ops, is_preview=True + ) as cmd_group: + cmd_group.command("show", "iot_pnp_model_show") + cmd_group.command("create", "iot_pnp_model_create") + cmd_group.command("publish", "iot_pnp_model_publish", confirmation=True) + cmd_group.command("list", "iot_pnp_model_list") diff --git a/azext_iot/pnp/commands_api.py b/azext_iot/pnp/commands_api.py new file mode 100644 index 000000000..271577424 --- /dev/null +++ b/azext_iot/pnp/commands_api.py @@ -0,0 +1,59 @@ +# 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.pnp.providers.model_repository_api import ModelApiProvider +from azext_iot.sdk.pnp.dataplane.models import ModelSearchOptions +from knack.util import CLIError +from azext_iot.common.utility import process_json_arg +from azext_iot.operations.generic import _process_top + + +def iot_pnp_model_show(cmd, model_id, expand=False, pnp_dns_suffix=None): + if not model_id: + raise CLIError("Please provide a model id [--model-id]") + ap = ModelApiProvider(cmd, pnp_dns_suffix) + return ap.get_model(model_id, expand) + + +def iot_pnp_model_list( + cmd, + keyword=None, + model_type=None, + model_state=None, + publisher_id=None, + created_by=None, + shared=False, + top=None, + pnp_dns_suffix=None, +): + ap = ModelApiProvider(cmd, pnp_dns_suffix) + search_options = ModelSearchOptions( + search_keyword=keyword, + model_type=model_type, + model_state=model_state, + publisher_id=publisher_id, + created_by=created_by, + ) + + return ap.search_models(search_options, shared, _process_top(top)) + + +def iot_pnp_model_create(cmd, model, pnp_dns_suffix=None): + if not model: + raise CLIError("Please provide a model definition [--model]") + ap = ModelApiProvider(cmd, pnp_dns_suffix) + model = process_json_arg(model, argument_name="model") + model_id = model.get("@id") + if not model_id: + raise CLIError("Model is invalid - @id attribute required.") + return ap.create_model(model_id, model) + + +def iot_pnp_model_publish(cmd, model_id, pnp_dns_suffix=None): + if not model_id: + raise CLIError("Please provide a model id [--model-id]") + ap = ModelApiProvider(cmd, pnp_dns_suffix) + return ap.publish_model(model_id=model_id) diff --git a/azext_iot/pnp/commands_repository.py b/azext_iot/pnp/commands_repository.py new file mode 100644 index 000000000..637b62b06 --- /dev/null +++ b/azext_iot/pnp/commands_repository.py @@ -0,0 +1,56 @@ +# 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.pnp.providers.resource import RepoResourceProvider +from azext_iot.sdk.pnp.modelrepository.models import Subject + + +def iot_pnp_tenant_create(cmd, pnp_dns_suffix=None): + rp = RepoResourceProvider(cmd, pnp_dns_suffix) + return rp.create() + + +def iot_pnp_tenant_show(cmd, pnp_dns_suffix=None): + rp = RepoResourceProvider(cmd, pnp_dns_suffix) + return rp.list() + + +def iot_pnp_role_create( + cmd, resource_id, resource_type, subject_id, subject_type, role, pnp_dns_suffix=None +): + rp = RepoResourceProvider(cmd, pnp_dns_suffix) + subject = Subject(subject_type=subject_type, role=role, resource_type=resource_type) + return rp.add_role_assignment( + resource_id=resource_id, + subject=subject, + subject_id=subject_id, + resource_type=resource_type, + ) + + +def iot_pnp_role_list( + cmd, resource_id, resource_type, subject_id=None, pnp_dns_suffix=None +): + rp = RepoResourceProvider(cmd, pnp_dns_suffix) + if subject_id: + return rp.get_role_assignments_for_subject( + resource_id=resource_id, subject_id=subject_id, resource_type=resource_type, + ) + return rp.get_role_assignments_for_resource( + resource_id=resource_id, resource_type=resource_type, + ) + + +def iot_pnp_role_delete( + cmd, resource_id, resource_type, role, subject_id, pnp_dns_suffix=None +): + rp = RepoResourceProvider(cmd, pnp_dns_suffix) + return rp.remove_role_assignment( + resource_id=resource_id, + subject_id=subject_id, + resource_type=resource_type, + role_id=role, + ) diff --git a/azext_iot/pnp/common.py b/azext_iot/pnp/common.py new file mode 100644 index 000000000..e3d0f8499 --- /dev/null +++ b/azext_iot/pnp/common.py @@ -0,0 +1,59 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +""" +shared: Define shared data types(enums) +""" + +from enum import Enum + + +class RoleResourceType(Enum): + """ + Type of the Resource + """ + + model = "Model" + tenant = "Tenant" + + +class RoleIdentifier(Enum): + """ + Role Identifier + """ + + modelsPublisher = "ModelsPublisher" + modelsCreator = "ModelsCreator" + tenantAdmin = "TenantAdministrator" + modelAdmin = "ModelAdministrator" + modelReader = "ModelReader" + + +class SubjectType(Enum): + """ + Subject type. + """ + + user = "User" + servicePrincipal = "ServicePrincipal" + + +class ModelType(Enum): + """ + Type of Model + """ + + interface = "Interface" + undetermined = "Undetermined" + + +class ModelState(Enum): + """ + State of a model + """ + + created = "Created" + listed = "Listed" diff --git a/azext_iot/pnp/params.py b/azext_iot/pnp/params.py new file mode 100644 index 000000000..fc204fc8b --- /dev/null +++ b/azext_iot/pnp/params.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. +# -------------------------------------------------------------------------------------------- + +""" +CLI parameter definitions. +""" + +from azext_iot.pnp.common import ( + RoleResourceType, + RoleIdentifier, + SubjectType, + ModelState, + ModelType, +) +from azure.cli.core.commands.parameters import get_enum_type, get_three_state_flag + + +def load_pnp_arguments(self, _): + """ + Load CLI Args for Knack parser + """ + with self.argument_context("iot pnp") as context: + context.argument( + "pnp_dns_suffix", + options_list=["--pnp-dns-suffix"], + help="An optional PnP DNS suffix used to interact with different PnP environments" + ) + + with self.argument_context("iot pnp role-assignment") as context: + context.argument( + "resource_id", + options_list=["--resource-id"], + help="The ID of the resource to manage role assignments for", + ) + context.argument( + "subject_id", + options_list=["--subject-id"], + help="The ID of a specific subject (User or Service Principal) to manage role assignments for.", + ) + context.argument( + "resource_type", + arg_type=get_enum_type(RoleResourceType), + options_list=["--resource-type"], + help="Resource Type for role", + ) + context.argument( + "role", + arg_type=get_enum_type(RoleIdentifier), + options_list=["--role"], + help="Role for assignment", + ) + with self.argument_context("iot pnp role-assignment create") as context: + context.argument( + "subject_type", + arg_type=get_enum_type(SubjectType), + options_list=["--subject-type"], + help="Subject Type for role assignment", + ) + with self.argument_context("iot pnp model") as context: + context.argument( + "model_id", + options_list=["--model-id", "--dtmi"], + help="Digital Twins model Id. Example: dtmi:example:Room;2", + ) + with self.argument_context("iot pnp model create") as context: + context.argument( + "model", + options_list=["--model"], + help="IoT Plug and Play capability-model definition written in DTDL (JSON-LD). " + "Can either be directly input or a file path where the content is extracted from.", + ) + with self.argument_context("iot pnp model show") as context: + context.argument( + "expand", + options_list=["--expand", "--def", "--definition"], + arg_type=get_three_state_flag(), + help="Expand the model’s referenced definitions inline", + ) + with self.argument_context("iot pnp model list") as context: + context.argument( + "keyword", + options_list=["--keyword", "-q"], + help="Restrict model list to those matching a provided keyword", + ) + context.argument( + "created_by", + options_list=["--created-by"], + help="Restrict model list to models created by a specific user or service principal", + ) + context.argument( + "model_state", + arg_type=get_enum_type(ModelState), + options_list=["--state", "--model-state"], + help="Restrict model list to models with a specific state", + ) + context.argument( + "model_type", + arg_type=get_enum_type(ModelType), + options_list=["--type", "--model-type"], + help="Restrict model list to models with a specific type", + ) + context.argument( + "publisher_id", + options_list=["--publisher-id", "--pub"], + help="Restrict model list to models published by a specific user or service principal", + ) + context.argument( + "shared", + arg_type=get_three_state_flag(), + options_list=["--shared"], + help="Restrict model list to shared models only", + ) diff --git a/azext_iot/pnp/providers/__init__.py b/azext_iot/pnp/providers/__init__.py new file mode 100644 index 000000000..7129949ce --- /dev/null +++ b/azext_iot/pnp/providers/__init__.py @@ -0,0 +1,65 @@ +# 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.sdk.pnp.modelrepository import ModelRepositoryControlPlaneApi +from azext_iot.sdk.pnp.dataplane import DigitalTwinModelRepositoryApi +from azext_iot.digitaltwins.providers.auth import DigitalTwinAuthentication +from azext_iot.constants import PNP_ENDPOINT, PNP_TENANT_RESOURCE_ID +from msrestazure.azure_exceptions import CloudError + +__all__ = [ + "pnp_modelrepo_service_factory", + "pnp_digitaltwin_modelrepo_api_service_factory", + "PnPModelRepositoryApiManager", + "PnPModelRepositoryManager", + "CloudError", +] + + +def pnp_modelrepo_service_factory(cmd, pnp_dns_suffix=None, *_): + """ + Factory for importing deps and getting service client resources. + Returns: + pnp_modelrepo_resource (ModelRepositoryControlPlaneApi): operational resource for + working with PnP Model Repository Tenants. + """ + creds = DigitalTwinAuthentication(cmd=cmd, resource_id=PNP_TENANT_RESOURCE_ID) + endpoint = "https://provider.{}".format( + pnp_dns_suffix if pnp_dns_suffix else PNP_ENDPOINT + ) + return ModelRepositoryControlPlaneApi(base_url=endpoint, credentials=creds) + + +class PnPModelRepositoryManager(object): + def __init__(self, cmd): + assert cmd + self.cmd = cmd + + def get_mgmt_sdk(self, pnp_dns_suffix=None): + return pnp_modelrepo_service_factory(self.cmd, pnp_dns_suffix) + + +def pnp_digitaltwin_modelrepo_api_service_factory(cmd, pnp_dns_suffix=None, *_): + """ + Factory for importing deps and getting service client resources. + Returns: + pnp_modelrepo_api_resource (DigitalTwinModelRepositoryApi): operational resource for + working with PnP Model Repository data. + """ + creds = DigitalTwinAuthentication(cmd=cmd, resource_id=PNP_TENANT_RESOURCE_ID) + endpoint = "https://repo.{}".format( + pnp_dns_suffix if pnp_dns_suffix else PNP_ENDPOINT + ) + return DigitalTwinModelRepositoryApi(base_url=endpoint, credentials=creds) + + +class PnPModelRepositoryApiManager(object): + def __init__(self, cmd): + assert cmd + self.cmd = cmd + + def get_mgmt_sdk(self, pnp_dns_suffix=None): + return pnp_digitaltwin_modelrepo_api_service_factory(self.cmd, pnp_dns_suffix) diff --git a/azext_iot/pnp/providers/model_repository_api.py b/azext_iot/pnp/providers/model_repository_api.py new file mode 100644 index 000000000..dfa56e1b1 --- /dev/null +++ b/azext_iot/pnp/providers/model_repository_api.py @@ -0,0 +1,89 @@ +# 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.pnp.providers import ( + PnPModelRepositoryApiManager, + CloudError, +) +from azext_iot.pnp.common import ModelState +from azext_iot.common.utility import unpack_msrest_error +from knack.util import CLIError + + +class ModelApiProvider(PnPModelRepositoryApiManager): + def __init__(self, cmd, pnp_dns_suffix=None): + super(ModelApiProvider, self).__init__(cmd=cmd) + self.mgmt_sdk = self.get_mgmt_sdk(pnp_dns_suffix) + + def get_model(self, model_id, expand=False): + try: + return self.mgmt_sdk.get_model_async( + model_id=model_id, expand=expand, raw=True + ).response.json() + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def create_model( + self, model_id, json_ld_model, + ): + try: + return self.mgmt_sdk.create_or_update_async( + model_id=model_id, + json_ld_model=json_ld_model, + x_ms_model_state=ModelState.created.value, + raw=True, + ).response.json() + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def publish_model( + self, model_id, + ): + try: + model_response = self.mgmt_sdk.get_model_async(model_id, raw=True) + etag = model_response.response.headers.get("eTag") + if not etag: + raise CLIError( + "No model found with @id `{}` to publish".format(model_id) + ) + etag = etag.replace('\\"', "") + return self.mgmt_sdk.create_or_update_async( + model_id=model_id, + update_metadata=True, + if_match=etag, + x_ms_model_state=ModelState.listed.value, + raw=True, + ).response.json() + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def search_models( + self, search_options, shared_models_only=None, top=None, + ): + try: + payload = [] + headers = {"Cache-Control": "no-cache, must-revalidate"} + + result = self.mgmt_sdk.search_models_async( + model_search_options=search_options, + x_ms_show_shared_models_only=shared_models_only, + custom_headers=headers, + raw=True, + ) + + payload.extend(result.response.json()) + + return payload[:top] if top else payload + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def validate_models(self, models=None, validate_dependencies=None): + try: + return self.mgmt_sdk.are_valid_models( + json_ld_models=models, validate_dependencies=validate_dependencies, + ) + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/pnp/providers/resource.py b/azext_iot/pnp/providers/resource.py new file mode 100644 index 000000000..bb42a7865 --- /dev/null +++ b/azext_iot/pnp/providers/resource.py @@ -0,0 +1,71 @@ +# 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.pnp.providers import ( + PnPModelRepositoryManager, + CloudError, +) +from azext_iot.common.utility import unpack_msrest_error +from knack.util import CLIError + + +class RepoResourceProvider(PnPModelRepositoryManager): + def __init__(self, cmd, pnp_dns_suffix=None): + super(RepoResourceProvider, self).__init__(cmd=cmd) + self.mgmt_sdk = self.get_mgmt_sdk(pnp_dns_suffix) + + def create(self): + try: + return self.mgmt_sdk.create_tenant_async(self) + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def list(self): + try: + return self.mgmt_sdk.get_tenant_async(self) + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + # RBAC + def get_role_assignments_for_resource(self, resource_id, resource_type): + try: + return self.mgmt_sdk.get_subjects_for_resources_async( + resource_id=resource_id, resource_type=resource_type, + ) + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def get_role_assignments_for_subject(self, resource_id, resource_type, subject_id): + try: + return self.mgmt_sdk.get_subjects_for_resources_async1( + resource_id=resource_id, + resource_type=resource_type, + subject_id=subject_id, + ) + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def add_role_assignment(self, resource_id, resource_type, subject_id, subject=None): + try: + return self.mgmt_sdk.assign_roles_async( + resource_id=resource_id, + resource_type=resource_type, + subject=subject, + subject_id=subject_id, + ) + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + + def remove_role_assignment(self, resource_id, resource_type, role_id, subject_id): + try: + return self.mgmt_sdk.remove_roles_async( + resource_id=resource_id, + resource_type=resource_type, + subject_id=subject_id, + role_id=role_id, + ) + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/sdk/iothub/pnp_runtime/__init__.py b/azext_iot/sdk/iothub/pnp_runtime/__init__.py new file mode 100644 index 000000000..41715539b --- /dev/null +++ b/azext_iot/sdk/iothub/pnp_runtime/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .iot_hub_gateway_service_ap_is import IotHubGatewayServiceAPIs +from .version import VERSION + +__all__ = ['IotHubGatewayServiceAPIs'] + +__version__ = VERSION + diff --git a/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py b/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py new file mode 100644 index 000000000..93ca28267 --- /dev/null +++ b/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py @@ -0,0 +1,91 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.service_client import SDKClient +from msrest import Serializer, Deserializer +from msrestazure import AzureConfiguration +from .version import VERSION +from .operations.digital_twin_operations import DigitalTwinOperations +from azext_iot.constants import USER_AGENT +from . import models + + +class IotHubGatewayServiceAPIsConfiguration(AzureConfiguration): + """Configuration for IotHubGatewayServiceAPIs + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if not base_url: + base_url = 'https://fully-qualified-iothubname.azure-devices.net' + + super(IotHubGatewayServiceAPIsConfiguration, self).__init__(base_url) + + self.add_user_agent('iothubgatewayserviceapis/{}'.format(VERSION)) + self.add_user_agent(USER_AGENT) # @digimaun + + self.credentials = credentials + + +class IotHubGatewayServiceAPIs(SDKClient): + """IotHubGatewayServiceAPIs + + :ivar config: Configuration for client. + :vartype config: IotHubGatewayServiceAPIsConfiguration + + :ivar configuration: Configuration operations + :vartype configuration: pnp_runtime.operations.ConfigurationOperations + :ivar registry_manager: RegistryManager operations + :vartype registry_manager: pnp_runtime.operations.RegistryManagerOperations + :ivar job_client: JobClient operations + :vartype job_client: pnp_runtime.operations.JobClientOperations + :ivar fault_injection: FaultInjection operations + :vartype fault_injection: pnp_runtime.operations.FaultInjectionOperations + :ivar twin: Twin operations + :vartype twin: pnp_runtime.operations.TwinOperations + :ivar digital_twin: DigitalTwin operations + :vartype digital_twin: pnp_runtime.operations.DigitalTwinOperations + :ivar http_runtime: HttpRuntime operations + :vartype http_runtime: pnp_runtime.operations.HttpRuntimeOperations + :ivar module_api: ModuleApi operations + :vartype module_api: pnp_runtime.operations.ModuleApiOperations + :ivar device_method: DeviceMethod operations + :vartype device_method: pnp_runtime.operations.DeviceMethodOperations + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + self.config = IotHubGatewayServiceAPIsConfiguration(credentials, base_url) + super(IotHubGatewayServiceAPIs, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2020-06-30-preview' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + self.digital_twin = DigitalTwinOperations( + self._client, self.config, self._serialize, self._deserialize) diff --git a/azext_iot/sdk/pnp/version.py b/azext_iot/sdk/iothub/pnp_runtime/models/__init__.py similarity index 96% rename from azext_iot/sdk/pnp/version.py rename to azext_iot/sdk/iothub/pnp_runtime/models/__init__.py index 8bf1b66a3..593333085 100644 --- a/azext_iot/sdk/pnp/version.py +++ b/azext_iot/sdk/iothub/pnp_runtime/models/__init__.py @@ -8,6 +8,3 @@ # Changes may cause incorrect behavior and will be lost if the code is # regenerated. # -------------------------------------------------------------------------- - -VERSION = "v1" - diff --git a/azext_iot/sdk/iothub/pnp_runtime/operations/__init__.py b/azext_iot/sdk/iothub/pnp_runtime/operations/__init__.py new file mode 100644 index 000000000..0de995247 --- /dev/null +++ b/azext_iot/sdk/iothub/pnp_runtime/operations/__init__.py @@ -0,0 +1,17 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twin_operations import DigitalTwinOperations + + +__all__ = [ + 'DigitalTwinOperations', +] diff --git a/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py b/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py new file mode 100644 index 000000000..c413aa7bd --- /dev/null +++ b/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py @@ -0,0 +1,332 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrestazure.azure_exceptions import CloudError + +# @digimaun - removed models references as no models are used. + + +class DigitalTwinOperations(object): + """DigitalTwinOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the Api. Constant value: "2020-06-30-preview". + """ + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-06-30-preview" + + self.config = config + + def get_digital_twin( + self, id, custom_headers=None, raw=False, **operation_config): + """Gets a digital twin. + + :param id: Digital Twin ID. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.get_digital_twin.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'ETag': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + get_digital_twin.metadata = {'url': '/digitaltwins/{id}'} + + def update_digital_twin( + self, id, digital_twin_patch, if_match=None, custom_headers=None, raw=False, **operation_config): + """Updates a digital twin. + + :param id: Digital Twin ID. + :type id: str + :param digital_twin_patch: json-patch contents to update. + :type digital_twin_patch: list[object] + :param if_match: + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.update_digital_twin.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(digital_twin_patch, '[object]') + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [202]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + if raw: + client_raw_response = ClientRawResponse(None, response) + client_raw_response.add_headers({ + 'ETag': 'str', + 'Location': 'str', + }) + return client_raw_response + update_digital_twin.metadata = {'url': '/digitaltwins/{id}'} + + def invoke_root_level_command( + self, id, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config): + """Invoke a digital twin root level command. + + Invoke a digital twin root level command. + + :param id: + :type id: str + :param command_name: + :type command_name: str + :param payload: + :type payload: object + :param connect_timeout_in_seconds: Maximum interval of time, in + seconds, that the digital twin command will wait for the answer. + :type connect_timeout_in_seconds: int + :param response_timeout_in_seconds: Maximum interval of time, in + seconds, that the digital twin command will wait for the answer. + :type response_timeout_in_seconds: int + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.invoke_root_level_command.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'commandName': self._serialize.url("command_name", command_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + if connect_timeout_in_seconds is not None: + query_parameters['connectTimeoutInSeconds'] = self._serialize.query("connect_timeout_in_seconds", connect_timeout_in_seconds, 'int') + if response_timeout_in_seconds is not None: + query_parameters['responseTimeoutInSeconds'] = self._serialize.query("response_timeout_in_seconds", response_timeout_in_seconds, 'int') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(payload, 'object') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'x-ms-command-statuscode': 'int', + 'x-ms-request-id': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + invoke_root_level_command.metadata = {'url': '/digitaltwins/{id}/commands/{commandName}'} + + def invoke_component_command( + self, id, component_path, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config): + """Invoke a digital twin command. + + Invoke a digital twin command. + + :param id: + :type id: str + :param component_path: + :type component_path: str + :param command_name: + :type command_name: str + :param payload: + :type payload: object + :param connect_timeout_in_seconds: Maximum interval of time, in + seconds, that the digital twin command will wait for the answer. + :type connect_timeout_in_seconds: int + :param response_timeout_in_seconds: Maximum interval of time, in + seconds, that the digital twin command will wait for the answer. + :type response_timeout_in_seconds: int + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.invoke_component_command.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'componentPath': self._serialize.url("component_path", component_path, 'str'), + 'commandName': self._serialize.url("command_name", command_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + if connect_timeout_in_seconds is not None: + query_parameters['connectTimeoutInSeconds'] = self._serialize.query("connect_timeout_in_seconds", connect_timeout_in_seconds, 'int') + if response_timeout_in_seconds is not None: + query_parameters['responseTimeoutInSeconds'] = self._serialize.query("response_timeout_in_seconds", response_timeout_in_seconds, 'int') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(payload, 'object') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('object', response) + header_dict = { + 'x-ms-command-statuscode': 'int', + 'x-ms-request-id': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + invoke_component_command.metadata = {'url': '/digitaltwins/{id}/components/{componentPath}/commands/{commandName}'} diff --git a/azext_iot/sdk/iothub/pnp_runtime/version.py b/azext_iot/sdk/iothub/pnp_runtime/version.py new file mode 100644 index 000000000..e2d2d7dc8 --- /dev/null +++ b/azext_iot/sdk/iothub/pnp_runtime/version.py @@ -0,0 +1,13 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +VERSION = "2020-06-30-preview" + diff --git a/azext_iot/sdk/pnp/__init__.py b/azext_iot/sdk/pnp/__init__.py index b77741974..55614acbf 100644 --- a/azext_iot/sdk/pnp/__init__.py +++ b/azext_iot/sdk/pnp/__init__.py @@ -1,18 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .digital_twin_repository_service import DigitalTwinRepositoryService -from .version import VERSION - -__all__ = ['DigitalTwinRepositoryService'] - -__version__ = VERSION - +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/azext_iot/sdk/pnp/dataplane/__init__.py b/azext_iot/sdk/pnp/dataplane/__init__.py new file mode 100644 index 000000000..918faacf0 --- /dev/null +++ b/azext_iot/sdk/pnp/dataplane/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .digital_twin_model_repository_api import DigitalTwinModelRepositoryApi +from .version import VERSION + +__all__ = ['DigitalTwinModelRepositoryApi'] + +__version__ = VERSION + diff --git a/azext_iot/sdk/pnp/dataplane/digital_twin_model_repository_api.py b/azext_iot/sdk/pnp/dataplane/digital_twin_model_repository_api.py new file mode 100644 index 000000000..bca99949b --- /dev/null +++ b/azext_iot/sdk/pnp/dataplane/digital_twin_model_repository_api.py @@ -0,0 +1,428 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.service_client import SDKClient +from msrest import Serializer, Deserializer +from msrestazure import AzureConfiguration +from .version import VERSION +from msrest.pipeline import ClientRawResponse +from msrestazure.azure_exceptions import CloudError +import uuid +from . import models +from azext_iot.constants import USER_AGENT + + +class DigitalTwinModelRepositoryApiConfiguration(AzureConfiguration): + """Configuration for DigitalTwinModelRepositoryApi + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if not base_url: + base_url = 'http://localhost' + + super(DigitalTwinModelRepositoryApiConfiguration, self).__init__(base_url) + + self.add_user_agent('digitaltwinmodelrepositoryapi/{}'.format(VERSION)) + self.add_user_agent(USER_AGENT) # @c-ryan-k + + self.credentials = credentials + + +class DigitalTwinModelRepositoryApi(SDKClient): + """DigitalTwinModelRepositoryApi + + :ivar config: Configuration for client. + :vartype config: DigitalTwinModelRepositoryApiConfiguration + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + self.config = DigitalTwinModelRepositoryApiConfiguration(credentials, base_url) + super(DigitalTwinModelRepositoryApi, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2020-05-01-preview' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + + def get_model_async( + self, model_id, expand=None, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Gets a Digital Twin model definition for the given digital twin model + Id. + + :param model_id: Digital Twin model Id e.g.: + dtmi:com:contoso:temperaturesensor;1. + :type model_id: str + :param expand: Gets or sets a value indicating whether indicates + whether to expand the model�s referenced definitions inline or not. + :type expand: bool + :param x_ms_client_request_id: Gets or sets optional. Provides a + client-generated value that is recorded in the logs. Using this header + is highly recommended for correlating client-side activities with + requests received by the server. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_model_async.metadata['url'] + path_format_arguments = { + 'modelId': self._serialize.url("model_id", model_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + if expand is not None: + query_parameters['expand'] = self._serialize.query("expand", expand, 'bool') + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/ld+json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 401, 404, 500]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('str', response) + if response.status_code == 401: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 404: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 500: + deserialized = self._deserialize('ServiceError', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_model_async.metadata = {'url': '/models/{modelId}'} + + def create_or_update_async( + self, model_id, json_ld_model=None, if_match=None, update_metadata=None, x_ms_model_state=None, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Creates a Digital Twin model or updates meta data properties of a + Digital Twin model. + + :param model_id: Digital Twin model Id e.g.: + dtmi:com:contoso:temperaturesensor;1. + :type model_id: str + :param json_ld_model: Model definition in Digital Twin Definition + Language (DTDL) format. + :type json_ld_model: object + :param if_match: Gets or sets model definition in Digital Twin + Definition Language (DTDL) format. + :type if_match: str + :param update_metadata: Gets or sets a value indicating whether used + to modify metadata properties supplied in the Header. + :type update_metadata: bool + :param x_ms_model_state: Gets or sets model Lifecycle state. Possible + values include: 'Created', 'Listed' + :type x_ms_model_state: str + :param x_ms_client_request_id: Gets or sets optional. Provides a + client-generated value that is recorded in the logs. Using this header + is highly recommended for correlating client-side activities with + requests received by the server. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.create_or_update_async.metadata['url'] + path_format_arguments = { + 'modelId': self._serialize.url("model_id", model_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + if update_metadata is not None: + query_parameters['update-metadata'] = self._serialize.query("update_metadata", update_metadata, 'bool') + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['if-match'] = self._serialize.header("if_match", if_match, 'str') + if x_ms_model_state is not None: + header_parameters['x-ms-model-state'] = self._serialize.header("x_ms_model_state", x_ms_model_state, 'str') + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if json_ld_model is not None: + body_content = self._serialize.body(json_ld_model, 'object') + else: + body_content = None + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 201, 401, 404, 409, 412, 415, 428, 500]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('str', response) + if response.status_code == 201: + deserialized = self._deserialize('str', response) + if response.status_code == 401: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 404: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 409: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 412: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 415: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 428: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 500: + deserialized = self._deserialize('ServiceError', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create_or_update_async.metadata = {'url': '/models/{modelId}'} + + def search_models_async( + self, model_search_options=None, x_ms_page_size=None, x_ms_show_shared_models_only=None, x_ms_continuation_token=None, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Queries Model repository for Digital Twin models matching search + options. + + :param model_search_options: Gets or sets model Search Options. + :type model_search_options: ~pnp.models.ModelSearchOptions + :param x_ms_page_size: Gets or sets when specified, it ensures that + paged results are returned. If not specified, default value is 50. + :type x_ms_page_size: int + :param x_ms_show_shared_models_only: Gets or sets a value indicating + whether [show shared models]. + :type x_ms_show_shared_models_only: bool + :param x_ms_continuation_token: Gets or sets when there are more + results than a page size, server responds with a continuation token. + Supply this token to retrieve next set of page results. + :type x_ms_continuation_token: str + :param x_ms_client_request_id: Gets or sets optional. Provides a + client-generated value that is recorded in the logs. Using this header + is highly recommended for correlating client-side activities with + requests received by the server. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.search_models_async.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/ld+json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_page_size is not None: + header_parameters['x-ms-page-size'] = self._serialize.header("x_ms_page_size", x_ms_page_size, 'int') + if x_ms_show_shared_models_only is not None: + header_parameters['x-ms-show-shared-models-only'] = self._serialize.header("x_ms_show_shared_models_only", x_ms_show_shared_models_only, 'bool') + if x_ms_continuation_token is not None: + header_parameters['x-ms-continuation-token'] = self._serialize.header("x_ms_continuation_token", x_ms_continuation_token, 'str') + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if model_search_options is not None: + body_content = self._serialize.body(model_search_options, 'ModelSearchOptions') + else: + body_content = None + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 404, 500]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('[ModelInformation]', response) + if response.status_code == 404: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 500: + deserialized = self._deserialize('ServiceError', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + search_models_async.metadata = {'url': '/models/search'} + + def are_valid_models( + self, json_ld_models=None, x_ms_client_request_id=None, validate_dependencies=None, custom_headers=None, raw=False, **operation_config): + """Validates the models according to the DTDL spec. + + :param json_ld_models: The json ld models. + :type json_ld_models: list[object] + :param x_ms_client_request_id: Gets or sets optional. Provides a + client-generated value that is recorded in the logs. Using this header + is highly recommended for correlating client-side activities with + requests received by the server. + :type x_ms_client_request_id: str + :param validate_dependencies: Gets or sets a value indicating whether + [validate dependencies]. + :type validate_dependencies: bool + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.are_valid_models.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + if validate_dependencies is not None: + query_parameters['validateDependencies'] = self._serialize.query("validate_dependencies", validate_dependencies, 'bool') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if json_ld_models is not None: + body_content = self._serialize.body(json_ld_models, '[object]') + else: + body_content = None + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400, 500]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('str', response) + if response.status_code == 400: + deserialized = self._deserialize('ServiceError', response) + if response.status_code == 500: + deserialized = self._deserialize('ServiceError', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + are_valid_models.metadata = {'url': '/models/validate'} diff --git a/azext_iot/sdk/pnp/models/__init__.py b/azext_iot/sdk/pnp/dataplane/models/__init__.py similarity index 72% rename from azext_iot/sdk/pnp/models/__init__.py rename to azext_iot/sdk/pnp/dataplane/models/__init__.py index 21a611417..004c583bd 100644 --- a/azext_iot/sdk/pnp/models/__init__.py +++ b/azext_iot/sdk/pnp/dataplane/models/__init__.py @@ -10,16 +10,16 @@ # -------------------------------------------------------------------------- try: - from .search_options_py3 import SearchOptions from .model_information_py3 import ModelInformation - from .search_response_py3 import SearchResponse + from .service_error_py3 import ServiceError + from .model_search_options_py3 import ModelSearchOptions except (SyntaxError, ImportError): - from .search_options import SearchOptions from .model_information import ModelInformation - from .search_response import SearchResponse + from .service_error import ServiceError + from .model_search_options import ModelSearchOptions __all__ = [ - 'SearchOptions', 'ModelInformation', - 'SearchResponse', + 'ServiceError', + 'ModelSearchOptions', ] diff --git a/azext_iot/sdk/pnp/dataplane/models/model_information.py b/azext_iot/sdk/pnp/dataplane/models/model_information.py new file mode 100644 index 000000000..c18382557 --- /dev/null +++ b/azext_iot/sdk/pnp/dataplane/models/model_information.py @@ -0,0 +1,112 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelInformation(Model): + """Model Metadata. + + :param comment: Gets or sets comments value in the model. + :type comment: str + :param description: Gets or sets description about the model. + :type description: str + :param display_name: Gets or sets display Name of the model. + :type display_name: str + :param model_id: Gets or sets model Identifier. + :type model_id: str + :param model_name: Gets or sets model Name. + :type model_name: str + :param version: Gets or sets modelVersion of the model. + :type version: str + :param model_type: Gets or sets model Definition Type. Possible values + include: 'Interface', 'Undetermined' + :type model_type: str or ~pnp.models.enum + :param publisher_id: Gets or sets aad Tenant Id of the model publisher. + :type publisher_id: str + :param publisher_name: Gets or sets aad Tenant Name of the model + publisher. + :type publisher_name: str + :param created_by: Gets or sets the identity of the user who created the + model. + :type created_by: str + :param created_date: Gets or sets created Date and Time of the model. + :type created_date: datetime + :param updated_date: Gets or sets updated date and time of the model. + :type updated_date: datetime + :param model_state: Gets or sets the state of the model. - Created or + Listed. Possible values include: 'Created', 'Listed' + :type model_state: str or ~pnp.models.enum + :param _etag: Gets or sets the e tag. + :type _etag: str + :param count_of_components: Gets or sets the number of components. + :type count_of_components: int + :param count_of_commands: Gets or sets the number of commands. + :type count_of_commands: int + :param count_of_telemetries: Gets or sets the number of telemetries. + :type count_of_telemetries: int + :param count_of_properties: Gets or sets the number of properties. + :type count_of_properties: int + :param count_of_relationships: Gets or sets the number of relationships. + :type count_of_relationships: int + :param count_of_extends: Gets or sets the number of extends. + :type count_of_extends: int + :param count_of_schemas: Gets or sets the number of schemas. + :type count_of_schemas: int + """ + + _attribute_map = { + 'comment': {'key': 'comment', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': 'str'}, + 'model_id': {'key': 'modelId', 'type': 'str'}, + 'model_name': {'key': 'modelName', 'type': 'str'}, + 'version': {'key': 'version', 'type': 'str'}, + 'model_type': {'key': 'modelType', 'type': 'str'}, + 'publisher_id': {'key': 'publisherId', 'type': 'str'}, + 'publisher_name': {'key': 'publisherName', 'type': 'str'}, + 'created_by': {'key': 'createdBy', 'type': 'str'}, + 'created_date': {'key': 'createdDate', 'type': 'iso-8601'}, + 'updated_date': {'key': 'updatedDate', 'type': 'iso-8601'}, + 'model_state': {'key': 'modelState', 'type': 'str'}, + '_etag': {'key': '_etag', 'type': 'str'}, + 'count_of_components': {'key': 'countOfComponents', 'type': 'int'}, + 'count_of_commands': {'key': 'countOfCommands', 'type': 'int'}, + 'count_of_telemetries': {'key': 'countOfTelemetries', 'type': 'int'}, + 'count_of_properties': {'key': 'countOfProperties', 'type': 'int'}, + 'count_of_relationships': {'key': 'countOfRelationships', 'type': 'int'}, + 'count_of_extends': {'key': 'countOfExtends', 'type': 'int'}, + 'count_of_schemas': {'key': 'countOfSchemas', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(ModelInformation, self).__init__(**kwargs) + self.comment = kwargs.get('comment', None) + self.description = kwargs.get('description', None) + self.display_name = kwargs.get('display_name', None) + self.model_id = kwargs.get('model_id', None) + self.model_name = kwargs.get('model_name', None) + self.version = kwargs.get('version', None) + self.model_type = kwargs.get('model_type', None) + self.publisher_id = kwargs.get('publisher_id', None) + self.publisher_name = kwargs.get('publisher_name', None) + self.created_by = kwargs.get('created_by', None) + self.created_date = kwargs.get('created_date', None) + self.updated_date = kwargs.get('updated_date', None) + self.model_state = kwargs.get('model_state', None) + self._etag = kwargs.get('_etag', None) + self.count_of_components = kwargs.get('count_of_components', None) + self.count_of_commands = kwargs.get('count_of_commands', None) + self.count_of_telemetries = kwargs.get('count_of_telemetries', None) + self.count_of_properties = kwargs.get('count_of_properties', None) + self.count_of_relationships = kwargs.get('count_of_relationships', None) + self.count_of_extends = kwargs.get('count_of_extends', None) + self.count_of_schemas = kwargs.get('count_of_schemas', None) diff --git a/azext_iot/sdk/pnp/dataplane/models/model_information_py3.py b/azext_iot/sdk/pnp/dataplane/models/model_information_py3.py new file mode 100644 index 000000000..225d563cc --- /dev/null +++ b/azext_iot/sdk/pnp/dataplane/models/model_information_py3.py @@ -0,0 +1,112 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelInformation(Model): + """Model Metadata. + + :param comment: Gets or sets comments value in the model. + :type comment: str + :param description: Gets or sets description about the model. + :type description: str + :param display_name: Gets or sets display Name of the model. + :type display_name: str + :param model_id: Gets or sets model Identifier. + :type model_id: str + :param model_name: Gets or sets model Name. + :type model_name: str + :param version: Gets or sets modelVersion of the model. + :type version: str + :param model_type: Gets or sets model Definition Type. Possible values + include: 'Interface', 'Undetermined' + :type model_type: str or ~pnp.models.enum + :param publisher_id: Gets or sets aad Tenant Id of the model publisher. + :type publisher_id: str + :param publisher_name: Gets or sets aad Tenant Name of the model + publisher. + :type publisher_name: str + :param created_by: Gets or sets the identity of the user who created the + model. + :type created_by: str + :param created_date: Gets or sets created Date and Time of the model. + :type created_date: datetime + :param updated_date: Gets or sets updated date and time of the model. + :type updated_date: datetime + :param model_state: Gets or sets the state of the model. - Created or + Listed. Possible values include: 'Created', 'Listed' + :type model_state: str or ~pnp.models.enum + :param _etag: Gets or sets the e tag. + :type _etag: str + :param count_of_components: Gets or sets the number of components. + :type count_of_components: int + :param count_of_commands: Gets or sets the number of commands. + :type count_of_commands: int + :param count_of_telemetries: Gets or sets the number of telemetries. + :type count_of_telemetries: int + :param count_of_properties: Gets or sets the number of properties. + :type count_of_properties: int + :param count_of_relationships: Gets or sets the number of relationships. + :type count_of_relationships: int + :param count_of_extends: Gets or sets the number of extends. + :type count_of_extends: int + :param count_of_schemas: Gets or sets the number of schemas. + :type count_of_schemas: int + """ + + _attribute_map = { + 'comment': {'key': 'comment', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': 'str'}, + 'model_id': {'key': 'modelId', 'type': 'str'}, + 'model_name': {'key': 'modelName', 'type': 'str'}, + 'version': {'key': 'version', 'type': 'str'}, + 'model_type': {'key': 'modelType', 'type': 'str'}, + 'publisher_id': {'key': 'publisherId', 'type': 'str'}, + 'publisher_name': {'key': 'publisherName', 'type': 'str'}, + 'created_by': {'key': 'createdBy', 'type': 'str'}, + 'created_date': {'key': 'createdDate', 'type': 'iso-8601'}, + 'updated_date': {'key': 'updatedDate', 'type': 'iso-8601'}, + 'model_state': {'key': 'modelState', 'type': 'str'}, + '_etag': {'key': '_etag', 'type': 'str'}, + 'count_of_components': {'key': 'countOfComponents', 'type': 'int'}, + 'count_of_commands': {'key': 'countOfCommands', 'type': 'int'}, + 'count_of_telemetries': {'key': 'countOfTelemetries', 'type': 'int'}, + 'count_of_properties': {'key': 'countOfProperties', 'type': 'int'}, + 'count_of_relationships': {'key': 'countOfRelationships', 'type': 'int'}, + 'count_of_extends': {'key': 'countOfExtends', 'type': 'int'}, + 'count_of_schemas': {'key': 'countOfSchemas', 'type': 'int'}, + } + + def __init__(self, *, comment: str=None, description: str=None, display_name: str=None, model_id: str=None, model_name: str=None, version: str=None, model_type=None, publisher_id: str=None, publisher_name: str=None, created_by: str=None, created_date=None, updated_date=None, model_state=None, _etag: str=None, count_of_components: int=None, count_of_commands: int=None, count_of_telemetries: int=None, count_of_properties: int=None, count_of_relationships: int=None, count_of_extends: int=None, count_of_schemas: int=None, **kwargs) -> None: + super(ModelInformation, self).__init__(**kwargs) + self.comment = comment + self.description = description + self.display_name = display_name + self.model_id = model_id + self.model_name = model_name + self.version = version + self.model_type = model_type + self.publisher_id = publisher_id + self.publisher_name = publisher_name + self.created_by = created_by + self.created_date = created_date + self.updated_date = updated_date + self.model_state = model_state + self._etag = _etag + self.count_of_components = count_of_components + self.count_of_commands = count_of_commands + self.count_of_telemetries = count_of_telemetries + self.count_of_properties = count_of_properties + self.count_of_relationships = count_of_relationships + self.count_of_extends = count_of_extends + self.count_of_schemas = count_of_schemas diff --git a/azext_iot/sdk/pnp/dataplane/models/model_search_options.py b/azext_iot/sdk/pnp/dataplane/models/model_search_options.py new file mode 100644 index 000000000..4e7180c8a --- /dev/null +++ b/azext_iot/sdk/pnp/dataplane/models/model_search_options.py @@ -0,0 +1,47 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelSearchOptions(Model): + """Model Search Options. + + :param search_keyword: Gets or sets keyword for model search. Searches + within pre-defined properties. + :type search_keyword: str + :param model_type: Gets or sets the type of the model. Possible values + include: 'Interface', 'Undetermined' + :type model_type: str or ~pnp.models.enum + :param model_state: Gets or sets the state of the model. Possible values + include: 'Created', 'Listed' + :type model_state: str or ~pnp.models.enum + :param publisher_id: Gets or sets the publisher identifier. + :type publisher_id: str + :param created_by: Gets or sets the created by. + :type created_by: str + """ + + _attribute_map = { + 'search_keyword': {'key': 'searchKeyword', 'type': 'str'}, + 'model_type': {'key': 'modelType', 'type': 'str'}, + 'model_state': {'key': 'modelState', 'type': 'str'}, + 'publisher_id': {'key': 'publisherId', 'type': 'str'}, + 'created_by': {'key': 'createdBy', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ModelSearchOptions, self).__init__(**kwargs) + self.search_keyword = kwargs.get('search_keyword', None) + self.model_type = kwargs.get('model_type', None) + self.model_state = kwargs.get('model_state', None) + self.publisher_id = kwargs.get('publisher_id', None) + self.created_by = kwargs.get('created_by', None) diff --git a/azext_iot/sdk/pnp/dataplane/models/model_search_options_py3.py b/azext_iot/sdk/pnp/dataplane/models/model_search_options_py3.py new file mode 100644 index 000000000..d32952302 --- /dev/null +++ b/azext_iot/sdk/pnp/dataplane/models/model_search_options_py3.py @@ -0,0 +1,47 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelSearchOptions(Model): + """Model Search Options. + + :param search_keyword: Gets or sets keyword for model search. Searches + within pre-defined properties. + :type search_keyword: str + :param model_type: Gets or sets the type of the model. Possible values + include: 'Interface', 'Undetermined' + :type model_type: str or ~pnp.models.enum + :param model_state: Gets or sets the state of the model. Possible values + include: 'Created', 'Listed' + :type model_state: str or ~pnp.models.enum + :param publisher_id: Gets or sets the publisher identifier. + :type publisher_id: str + :param created_by: Gets or sets the created by. + :type created_by: str + """ + + _attribute_map = { + 'search_keyword': {'key': 'searchKeyword', 'type': 'str'}, + 'model_type': {'key': 'modelType', 'type': 'str'}, + 'model_state': {'key': 'modelState', 'type': 'str'}, + 'publisher_id': {'key': 'publisherId', 'type': 'str'}, + 'created_by': {'key': 'createdBy', 'type': 'str'}, + } + + def __init__(self, *, search_keyword: str=None, model_type=None, model_state=None, publisher_id: str=None, created_by: str=None, **kwargs) -> None: + super(ModelSearchOptions, self).__init__(**kwargs) + self.search_keyword = search_keyword + self.model_type = model_type + self.model_state = model_state + self.publisher_id = publisher_id + self.created_by = created_by diff --git a/azext_iot/sdk/pnp/models/search_response.py b/azext_iot/sdk/pnp/dataplane/models/service_error.py similarity index 51% rename from azext_iot/sdk/pnp/models/search_response.py rename to azext_iot/sdk/pnp/dataplane/models/service_error.py index a6e36d521..1e66b591d 100644 --- a/azext_iot/sdk/pnp/models/search_response.py +++ b/azext_iot/sdk/pnp/dataplane/models/service_error.py @@ -12,22 +12,21 @@ from msrest.serialization import Model -class SearchResponse(Model): - """SearchResponse. +class ServiceError(Model): + """Error Class. - :param continuation_token: - :type continuation_token: str - :param results: - :type results: - list[~digitaltwinmodelrepositoryservice.models.ModelInformation] + :param code: Gets or sets the code. + :type code: str + :param message: Gets or sets the message. + :type message: str """ _attribute_map = { - 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, - 'results': {'key': 'results', 'type': '[ModelInformation]'}, + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, } def __init__(self, **kwargs): - super(SearchResponse, self).__init__(**kwargs) - self.continuation_token = kwargs.get('continuation_token', None) - self.results = kwargs.get('results', None) + super(ServiceError, self).__init__(**kwargs) + self.code = kwargs.get('code', None) + self.message = kwargs.get('message', None) diff --git a/azext_iot/sdk/pnp/dataplane/models/service_error_py3.py b/azext_iot/sdk/pnp/dataplane/models/service_error_py3.py new file mode 100644 index 000000000..f7bae1062 --- /dev/null +++ b/azext_iot/sdk/pnp/dataplane/models/service_error_py3.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ServiceError(Model): + """Error Class. + + :param code: Gets or sets the code. + :type code: str + :param message: Gets or sets the message. + :type message: str + """ + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + } + + def __init__(self, *, code: str=None, message: str=None, **kwargs) -> None: + super(ServiceError, self).__init__(**kwargs) + self.code = code + self.message = message diff --git a/azext_iot/sdk/pnp/dataplane/version.py b/azext_iot/sdk/pnp/dataplane/version.py new file mode 100644 index 000000000..fc729cd31 --- /dev/null +++ b/azext_iot/sdk/pnp/dataplane/version.py @@ -0,0 +1,13 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +VERSION = "2020-05-01-preview" + diff --git a/azext_iot/sdk/pnp/digital_twin_repository_service.py b/azext_iot/sdk/pnp/digital_twin_repository_service.py deleted file mode 100644 index 9c1bf8e1f..000000000 --- a/azext_iot/sdk/pnp/digital_twin_repository_service.py +++ /dev/null @@ -1,361 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.service_client import SDKClient -from msrest import Configuration, Serializer, Deserializer -from .version import VERSION -from msrest.pipeline import ClientRawResponse -from msrest.exceptions import HttpOperationError -from . import models -from azext_iot.constants import USER_AGENT - - -class DigitalTwinRepositoryServiceConfiguration(Configuration): - """Configuration for DigitalTwinRepositoryService - Note that all parameters used to create this instance are saved as instance - attributes. - - :param str base_url: Service URL - """ - - def __init__( - self, base_url=None): - - if not base_url: - base_url = 'http://localhost' - - super(DigitalTwinRepositoryServiceConfiguration, self).__init__(base_url) - self.add_user_agent('digitaltwinrepositoryservice/{}'.format(VERSION)) - self.add_user_agent(USER_AGENT) - - -class DigitalTwinRepositoryService(SDKClient): - """DigitalTwin Model Repository Service. - - :ivar config: Configuration for client. - :vartype config: DigitalTwinRepositoryServiceConfiguration - - :param str base_url: Service URL - """ - - def __init__( - self, base_url=None): - - self.config = DigitalTwinRepositoryServiceConfiguration(base_url) - super(DigitalTwinRepositoryService, self).__init__(None, self.config) - - client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = 'v1' - self._serialize = Serializer(client_models) - self._deserialize = Deserializer(client_models) - - - def get_model( - self, model_id, api_version, repository_id=None, x_ms_client_request_id=None, expand=False, custom_headers=None, raw=False, **operation_config): - """Returns a DigitalTwin model object for the given \"id\".\r\nIf - \"expand\" is present in the query parameters and \"id\" is for a - capability model then it returns\r\nthe capability model with expanded - interface definitions. - - :param model_id: Model id Ex: - urn:contoso:com:temparaturesensor:1 - :type model_id: str - :param api_version: Version of the Api. Must be 2019-07-01-preview - :type api_version: str - :param repository_id: To access private repo, repositoryId is the repo - id. To access global repo, caller should not specify this value. - :type repository_id: str - :param x_ms_client_request_id: Optional. Provides a client-generated - opaque value that is recorded in the logs. Using this header is highly - recommended for correlating client-side activities with requests - received by the server. - :type x_ms_client_request_id: str - :param expand: Indicates whether to expand the capability model's - interface definitions inline or not. This query parameter ONLY applies - to Capability model. - :type expand: bool - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`HttpOperationError` - """ - # Construct URL - url = self.get_model.metadata['url'] - path_format_arguments = { - 'modelId': self._serialize.url("model_id", model_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - if repository_id is not None: - query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str') - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - if expand is not None: - query_parameters['expand'] = self._serialize.query("expand", expand, 'bool') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/ld+json' - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - raise HttpOperationError(self._deserialize, response) - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('{object}', response) - header_dict = { - 'x-ms-request-id': 'str', - 'ETag': 'str', - 'x-ms-model-id': 'str', - 'x-ms-model-publisher-id': 'str', - 'x-ms-model-publisher-name': 'str', - 'x-ms-model-createdon': 'iso-8601', - 'x-ms-model-lastupdated': 'iso-8601', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - get_model.metadata = {'url': '/models/{modelId}'} - - def create_or_update_model( - self, model_id, api_version, content, repository_id=None, x_ms_client_request_id=None, if_match=None, custom_headers=None, raw=False, **operation_config): - """Creates or updates the DigitalTwin Model in the repository. - - :param model_id: Model id Ex: - urn:contoso:TemparatureSensor:1 - :type model_id: str - :param api_version: Version of the Api. Must be 2019-07-01-preview - :type api_version: str - :param content: Model definition in Digital Twin Definition Language - format. - :type content: object - :param repository_id: To access private repo, repositoryId is the repo - id\\r\\nTo access global repo, caller should not specify this value. - :type repository_id: str - :param x_ms_client_request_id: Optional. Provides a client-generated - opaque value that is recorded in the logs. Using this header is highly - recommended for correlating client-side activities with requests - received by the server. - :type x_ms_client_request_id: str - :param if_match: Used to make operation conditional for optimistic - concurrency. That is, the document is updated only if the specified - etag matches the current version in the database. The value should be - set to the etag value of the resource. - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`HttpOperationError` - """ - # Construct URL - url = self.create_or_update_model.metadata['url'] - path_format_arguments = { - 'modelId': self._serialize.url("model_id", model_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - if repository_id is not None: - query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str') - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - - # Construct body - body_content = self._serialize.body(content, 'object') - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [201, 204, 412]: - raise HttpOperationError(self._deserialize, response) - - if raw: - client_raw_response = ClientRawResponse(None, response) - client_raw_response.add_headers({ - 'x-ms-request-id': 'str', - 'ETag': 'str', - }) - return client_raw_response - create_or_update_model.metadata = {'url': '/models/{modelId}'} - - def delete_model( - self, model_id, repository_id, api_version, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Deletes a digital twin model from the repository. - - :param model_id: Model id Ex: - urn:contoso:com:temparaturesensor:1 - :type model_id: str - :param repository_id: To access private repo, repositoryId is the repo - id. Delete is not allowed for public repository. - :type repository_id: str - :param api_version: Version of the Api. Must be 2019-07-01-preview - :type api_version: str - :param x_ms_client_request_id: Optional. Provides a client-generated - opaque value that is recorded in the logs. Using this header is highly - recommended for correlating client-side activities with requests - received by the server. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`HttpOperationError` - """ - # Construct URL - url = self.delete_model.metadata['url'] - path_format_arguments = { - 'modelId': self._serialize.url("model_id", model_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str') - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [204]: - raise HttpOperationError(self._deserialize, response) - - if raw: - client_raw_response = ClientRawResponse(None, response) - client_raw_response.add_headers({ - 'x-ms-request-id': 'str', - }) - return client_raw_response - delete_model.metadata = {'url': '/models/{modelId}'} - - def search( - self, search_options, api_version, repository_id=None, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Searches pnp models for given search options. - It searches in the "Description, DisplayName, Comment and Id" metadata. - - :param search_options: Set SearchOption.searchKeyword to search models - with the keyword. - Set the "SearchOptions.modelFilterType" to restrict to a type of - DigitalTwin model (Ex: Interface or CapabilityModel). - Default it returns all the models. - :type search_options: - ~digitaltwinmodelrepositoryservice.models.SearchOptions - :param api_version: Version of the Api. Must be 2019-07-01-preview - :type api_version: str - :param repository_id: To access private repo, repositoryId is the repo - id.\\r\\nDelete is not allowed for public repository. - :type repository_id: str - :param x_ms_client_request_id: Optional. Provides a client-generated - opaque value that is recorded in the logs. Using this header is highly - recommended for correlating client-side activities with requests - received by the server.. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: SearchResponse or ClientRawResponse if raw=true - :rtype: ~digitaltwinmodelrepositoryservice.models.SearchResponse or - ~msrest.pipeline.ClientRawResponse - :raises: - :class:`HttpOperationError` - """ - # Construct URL - url = self.search.metadata['url'] - - # Construct parameters - query_parameters = {} - if repository_id is not None: - query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str') - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - - # Construct body - body_content = self._serialize.body(search_options, 'SearchOptions') - - # Construct and send request - request = self._client.post(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - raise HttpOperationError(self._deserialize, response) - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('SearchResponse', response) - header_dict = { - 'x-ms-request-id': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - search.metadata = {'url': '/models/search'} diff --git a/azext_iot/sdk/pnp/modelrepository/__init__.py b/azext_iot/sdk/pnp/modelrepository/__init__.py new file mode 100644 index 000000000..9902856bd --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .model_repository_control_plane_api import ModelRepositoryControlPlaneApi +from .version import VERSION + +__all__ = ['ModelRepositoryControlPlaneApi'] + +__version__ = VERSION + diff --git a/azext_iot/sdk/pnp/modelrepository/model_repository_control_plane_api.py b/azext_iot/sdk/pnp/modelrepository/model_repository_control_plane_api.py new file mode 100644 index 000000000..e9c73ae4b --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/model_repository_control_plane_api.py @@ -0,0 +1,479 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.service_client import SDKClient +from msrest import Serializer, Deserializer +from msrestazure import AzureConfiguration +from .version import VERSION +from msrest.pipeline import ClientRawResponse +from msrestazure.azure_exceptions import CloudError +import uuid +from . import models +from azext_iot.constants import USER_AGENT + +class ModelRepositoryControlPlaneApiConfiguration(AzureConfiguration): + """Configuration for ModelRepositoryControlPlaneApi + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if not base_url: + base_url = 'http://localhost' + + super(ModelRepositoryControlPlaneApiConfiguration, self).__init__(base_url) + + self.add_user_agent('modelrepositorycontrolplaneapi/{}'.format(VERSION)) + self.add_user_agent(USER_AGENT) # @c-ryan-k + + self.credentials = credentials + + +class ModelRepositoryControlPlaneApi(SDKClient): + """ModelRepositoryControlPlaneApi + + :ivar config: Configuration for client. + :vartype config: ModelRepositoryControlPlaneApiConfiguration + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + self.config = ModelRepositoryControlPlaneApiConfiguration(credentials, base_url) + super(ModelRepositoryControlPlaneApi, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2020-05-01-preview' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + + def get_subjects_for_resources_async( + self, resource_id, resource_type, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Get the access permission for resource. + + :param resource_id: The resource identifier. + :type resource_id: str + :param resource_type: The resource Type. Possible values include: + 'Model', 'Tenant' + :type resource_type: str + :param x_ms_client_request_id: Gets or sets optional. Provides a + client-generated value that is recorded in the logs. Using this header + is highly recommended for correlating client-side activities with + requests received by the server. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: list or ClientRawResponse if raw=true + :rtype: list[~pnp.models.Target] or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_subjects_for_resources_async.metadata['url'] + path_format_arguments = { + 'resourceId': self._serialize.url("resource_id", resource_id, 'str'), + 'resourceType': self._serialize.url("resource_type", resource_type, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('[Target]', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_subjects_for_resources_async.metadata = {'url': '/resources/{resourceId}/types/{resourceType}/targets'} + + def get_subjects_for_resources_async1( + self, resource_id, subject_id, resource_type, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Get the access permission for resource for the specified + subject/principal. + + :param resource_id: The resource Id. + :type resource_id: str + :param subject_id: The Subject Id. + :type subject_id: str + :param resource_type: The resource Type. Possible values include: + 'Model', 'Tenant' + :type resource_type: str + :param x_ms_client_request_id: Gets or sets optional. Provides a + client-generated value that is recorded in the logs. Using this header + is highly recommended for correlating client-side activities with + requests received by the server. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: list or ClientRawResponse if raw=true + :rtype: list[~pnp.models.Target] or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_subjects_for_resources_async1.metadata['url'] + path_format_arguments = { + 'resourceId': self._serialize.url("resource_id", resource_id, 'str'), + 'subjectId': self._serialize.url("subject_id", subject_id, 'str'), + 'resourceType': self._serialize.url("resource_type", resource_type, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('[Target]', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_subjects_for_resources_async1.metadata = {'url': '/resources/{resourceId}/types/{resourceType}/subjects/{subjectId}/targets'} + + def assign_roles_async( + self, resource_id, resource_type, subject_id, x_ms_client_request_id=None, subject=None, custom_headers=None, raw=False, **operation_config): + """Assign roles and permissions for a subject/principal to a resource. + + :param resource_id: The resource identifier. + :type resource_id: str + :param resource_type: The resource type. Possible values include: + 'Model', 'Tenant' + :type resource_type: str + :param subject_id: The subject identifier. + :type subject_id: str + :param x_ms_client_request_id: Gets or sets optional clients request. + Provides a client-generated value that is recorded in the logs. Using + this header is highly recommended for correlating client-side + activities with requests received by the server. + :type x_ms_client_request_id: str + :param subject: Gets or sets the subject. + :type subject: ~pnp.models.Subject + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.assign_roles_async.metadata['url'] + path_format_arguments = { + 'resourceId': self._serialize.url("resource_id", resource_id, 'str'), + 'resourceType': self._serialize.url("resource_type", resource_type, 'str'), + 'subjectId': self._serialize.url("subject_id", subject_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if subject is not None: + body_content = self._serialize.body(subject, 'Subject') + else: + body_content = None + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + assign_roles_async.metadata = {'url': '/resources/{resourceId}/types/{resourceType}/subjects/{subjectId}'} + + def remove_roles_async( + self, resource_id, subject_id, resource_type, role_id, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Removes the roles assigned to a subject/principal. + + :param resource_id: The resource identifier. + :type resource_id: str + :param subject_id: The subject identifier. + :type subject_id: str + :param resource_type: The resource type. Possible values include: + 'Model', 'Tenant' + :type resource_type: str + :param role_id: The roleIdentifier. Possible values include: + 'ModelsPublisher', 'ModelsCreator', 'TenantAdministrator', + 'ModelAdministrator', 'ModelReader', 'None' + :type role_id: str + :param x_ms_client_request_id: Gets or sets optional. Provides a + client-generated value that is recorded in the logs. Using this header + is highly recommended for correlating client-side activities with + requests received by the server. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.remove_roles_async.metadata['url'] + path_format_arguments = { + 'resourceId': self._serialize.url("resource_id", resource_id, 'str'), + 'subjectId': self._serialize.url("subject_id", subject_id, 'str'), + 'resourceType': self._serialize.url("resource_type", resource_type, 'str'), + 'roleId': self._serialize.url("role_id", role_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + remove_roles_async.metadata = {'url': '/resources/{resourceId}/types/{resourceType}/subjects/{subjectId}/roles/{roleId}'} + + def get_tenant_async( + self, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Gets the information about the tenant. + + :param x_ms_client_request_id: Gets or sets optional. Provides a + client-generated value that is recorded in the logs. Using this header + is highly recommended for correlating client-side activities with + requests received by the server. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: list or ClientRawResponse if raw=true + :rtype: list[~pnp.models.Tenant] or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_tenant_async.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('[Tenant]', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_tenant_async.metadata = {'url': '/tenants'} + + def create_tenant_async( + self, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): + """Creates a new tenant. + + :param x_ms_client_request_id: Gets or sets optional. Provides a + client-generated value that is recorded in the logs. Using this header + is highly recommended for correlating client-side activities with + requests received by the server. + :type x_ms_client_request_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: Tenant or ClientRawResponse if raw=true + :rtype: ~pnp.models.Tenant or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.create_tenant_async.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_client_request_id is not None: + header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('Tenant', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create_tenant_async.metadata = {'url': '/tenants'} diff --git a/azext_iot/sdk/pnp/modelrepository/models/__init__.py b/azext_iot/sdk/pnp/modelrepository/models/__init__.py new file mode 100644 index 000000000..c835d715f --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/models/__init__.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +try: + from .subject_metadata_py3 import SubjectMetadata + from .subject_py3 import Subject + from .target_py3 import Target + from .tenant_py3 import Tenant +except (SyntaxError, ImportError): + from .subject_metadata import SubjectMetadata + from .subject import Subject + from .target import Target + from .tenant import Tenant + +__all__ = [ + 'SubjectMetadata', + 'Subject', + 'Target', + 'Tenant', +] diff --git a/azext_iot/sdk/pnp/modelrepository/models/subject.py b/azext_iot/sdk/pnp/modelrepository/models/subject.py new file mode 100644 index 000000000..e0c712547 --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/models/subject.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Subject(Model): + """Target. + + :param subject_type: Gets or sets the type. Possible values include: + 'User', 'ServicePrincipal' + :type subject_type: str or ~pnp.models.enum + :param role: Gets or sets Roles. Possible values include: + 'ModelsPublisher', 'ModelsCreator', 'TenantAdministrator', + 'ModelAdministrator', 'ModelReader', 'None' + :type role: str or ~pnp.models.enum + :param resource_type: Gets or sets the type of the resource. Possible + values include: 'Model', 'Tenant' + :type resource_type: str or ~pnp.models.enum + """ + + _attribute_map = { + 'subject_type': {'key': 'subjectType', 'type': 'str'}, + 'role': {'key': 'role', 'type': 'str'}, + 'resource_type': {'key': 'resourceType', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(Subject, self).__init__(**kwargs) + self.subject_type = kwargs.get('subject_type', None) + self.role = kwargs.get('role', None) + self.resource_type = kwargs.get('resource_type', None) diff --git a/azext_iot/sdk/pnp/modelrepository/models/subject_metadata.py b/azext_iot/sdk/pnp/modelrepository/models/subject_metadata.py new file mode 100644 index 000000000..45237cdc9 --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/models/subject_metadata.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SubjectMetadata(Model): + """Subject Metadata. + + :param resource_id: Gets or sets the resource identifier. + :type resource_id: str + :param subject_id: Gets or sets the subject identifier. + :type subject_id: str + """ + + _attribute_map = { + 'resource_id': {'key': 'resourceId', 'type': 'str'}, + 'subject_id': {'key': 'subjectId', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(SubjectMetadata, self).__init__(**kwargs) + self.resource_id = kwargs.get('resource_id', None) + self.subject_id = kwargs.get('subject_id', None) diff --git a/azext_iot/sdk/pnp/modelrepository/models/subject_metadata_py3.py b/azext_iot/sdk/pnp/modelrepository/models/subject_metadata_py3.py new file mode 100644 index 000000000..94dd4896b --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/models/subject_metadata_py3.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SubjectMetadata(Model): + """Subject Metadata. + + :param resource_id: Gets or sets the resource identifier. + :type resource_id: str + :param subject_id: Gets or sets the subject identifier. + :type subject_id: str + """ + + _attribute_map = { + 'resource_id': {'key': 'resourceId', 'type': 'str'}, + 'subject_id': {'key': 'subjectId', 'type': 'str'}, + } + + def __init__(self, *, resource_id: str=None, subject_id: str=None, **kwargs) -> None: + super(SubjectMetadata, self).__init__(**kwargs) + self.resource_id = resource_id + self.subject_id = subject_id diff --git a/azext_iot/sdk/pnp/modelrepository/models/subject_py3.py b/azext_iot/sdk/pnp/modelrepository/models/subject_py3.py new file mode 100644 index 000000000..38e945858 --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/models/subject_py3.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Subject(Model): + """Target. + + :param subject_type: Gets or sets the type. Possible values include: + 'User', 'ServicePrincipal' + :type subject_type: str or ~pnp.models.enum + :param role: Gets or sets Roles. Possible values include: + 'ModelsPublisher', 'ModelsCreator', 'TenantAdministrator', + 'ModelAdministrator', 'ModelReader', 'None' + :type role: str or ~pnp.models.enum + :param resource_type: Gets or sets the type of the resource. Possible + values include: 'Model', 'Tenant' + :type resource_type: str or ~pnp.models.enum + """ + + _attribute_map = { + 'subject_type': {'key': 'subjectType', 'type': 'str'}, + 'role': {'key': 'role', 'type': 'str'}, + 'resource_type': {'key': 'resourceType', 'type': 'str'}, + } + + def __init__(self, *, subject_type=None, role=None, resource_type=None, **kwargs) -> None: + super(Subject, self).__init__(**kwargs) + self.subject_type = subject_type + self.role = role + self.resource_type = resource_type diff --git a/azext_iot/sdk/pnp/modelrepository/models/target.py b/azext_iot/sdk/pnp/modelrepository/models/target.py new file mode 100644 index 000000000..a48f54887 --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/models/target.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Target(Model): + """Target class. + + :param subject_metadata: Gets or sets the subject meta data. + :type subject_metadata: ~pnp.models.SubjectMetadata + :param subject: Gets or sets the subject. + :type subject: ~pnp.models.Subject + """ + + _attribute_map = { + 'subject_metadata': {'key': 'subjectMetadata', 'type': 'SubjectMetadata'}, + 'subject': {'key': 'subject', 'type': 'Subject'}, + } + + def __init__(self, **kwargs): + super(Target, self).__init__(**kwargs) + self.subject_metadata = kwargs.get('subject_metadata', None) + self.subject = kwargs.get('subject', None) diff --git a/azext_iot/sdk/pnp/modelrepository/models/target_py3.py b/azext_iot/sdk/pnp/modelrepository/models/target_py3.py new file mode 100644 index 000000000..ea6ea322e --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/models/target_py3.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Target(Model): + """Target class. + + :param subject_metadata: Gets or sets the subject meta data. + :type subject_metadata: ~pnp.models.SubjectMetadata + :param subject: Gets or sets the subject. + :type subject: ~pnp.models.Subject + """ + + _attribute_map = { + 'subject_metadata': {'key': 'subjectMetadata', 'type': 'SubjectMetadata'}, + 'subject': {'key': 'subject', 'type': 'Subject'}, + } + + def __init__(self, *, subject_metadata=None, subject=None, **kwargs) -> None: + super(Target, self).__init__(**kwargs) + self.subject_metadata = subject_metadata + self.subject = subject diff --git a/azext_iot/sdk/pnp/modelrepository/models/tenant.py b/azext_iot/sdk/pnp/modelrepository/models/tenant.py new file mode 100644 index 000000000..521a9e363 --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/models/tenant.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Tenant(Model): + """Tenant Class. + + :param azure_ad_tenant_name: Gets or sets the name of the azure ad tenant. + :type azure_ad_tenant_name: str + :param azure_ad_tenant_id: Gets or sets the azure ad tenant identifier. + :type azure_ad_tenant_id: str + :param microsoft_partner_network_id: Gets or sets the microsoft partner + network identifier. + :type microsoft_partner_network_id: str + :param number_of_models_created: Gets or sets the number of models + created. + :type number_of_models_created: int + :param number_of_models_published: Gets or sets the number of models + published. + :type number_of_models_published: int + :param tenant_id: Gets or sets the tenant identifier. + :type tenant_id: str + """ + + _attribute_map = { + 'azure_ad_tenant_name': {'key': 'azureAdTenantName', 'type': 'str'}, + 'azure_ad_tenant_id': {'key': 'azureAdTenantId', 'type': 'str'}, + 'microsoft_partner_network_id': {'key': 'microsoftPartnerNetworkId', 'type': 'str'}, + 'number_of_models_created': {'key': 'numberOfModelsCreated', 'type': 'int'}, + 'number_of_models_published': {'key': 'numberOfModelsPublished', 'type': 'int'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(Tenant, self).__init__(**kwargs) + self.azure_ad_tenant_name = kwargs.get('azure_ad_tenant_name', None) + self.azure_ad_tenant_id = kwargs.get('azure_ad_tenant_id', None) + self.microsoft_partner_network_id = kwargs.get('microsoft_partner_network_id', None) + self.number_of_models_created = kwargs.get('number_of_models_created', None) + self.number_of_models_published = kwargs.get('number_of_models_published', None) + self.tenant_id = kwargs.get('tenant_id', None) diff --git a/azext_iot/sdk/pnp/modelrepository/models/tenant_py3.py b/azext_iot/sdk/pnp/modelrepository/models/tenant_py3.py new file mode 100644 index 000000000..43ab56fd9 --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/models/tenant_py3.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Tenant(Model): + """Tenant Class. + + :param azure_ad_tenant_name: Gets or sets the name of the azure ad tenant. + :type azure_ad_tenant_name: str + :param azure_ad_tenant_id: Gets or sets the azure ad tenant identifier. + :type azure_ad_tenant_id: str + :param microsoft_partner_network_id: Gets or sets the microsoft partner + network identifier. + :type microsoft_partner_network_id: str + :param number_of_models_created: Gets or sets the number of models + created. + :type number_of_models_created: int + :param number_of_models_published: Gets or sets the number of models + published. + :type number_of_models_published: int + :param tenant_id: Gets or sets the tenant identifier. + :type tenant_id: str + """ + + _attribute_map = { + 'azure_ad_tenant_name': {'key': 'azureAdTenantName', 'type': 'str'}, + 'azure_ad_tenant_id': {'key': 'azureAdTenantId', 'type': 'str'}, + 'microsoft_partner_network_id': {'key': 'microsoftPartnerNetworkId', 'type': 'str'}, + 'number_of_models_created': {'key': 'numberOfModelsCreated', 'type': 'int'}, + 'number_of_models_published': {'key': 'numberOfModelsPublished', 'type': 'int'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + } + + def __init__(self, *, azure_ad_tenant_name: str=None, azure_ad_tenant_id: str=None, microsoft_partner_network_id: str=None, number_of_models_created: int=None, number_of_models_published: int=None, tenant_id: str=None, **kwargs) -> None: + super(Tenant, self).__init__(**kwargs) + self.azure_ad_tenant_name = azure_ad_tenant_name + self.azure_ad_tenant_id = azure_ad_tenant_id + self.microsoft_partner_network_id = microsoft_partner_network_id + self.number_of_models_created = number_of_models_created + self.number_of_models_published = number_of_models_published + self.tenant_id = tenant_id diff --git a/azext_iot/sdk/pnp/modelrepository/version.py b/azext_iot/sdk/pnp/modelrepository/version.py new file mode 100644 index 000000000..bc2258ad0 --- /dev/null +++ b/azext_iot/sdk/pnp/modelrepository/version.py @@ -0,0 +1,13 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +VERSION = "2020-05-01" + diff --git a/azext_iot/sdk/pnp/models/model_information.py b/azext_iot/sdk/pnp/models/model_information.py deleted file mode 100644 index c3f804945..000000000 --- a/azext_iot/sdk/pnp/models/model_information.py +++ /dev/null @@ -1,72 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ModelInformation(Model): - """ModelInformation. - - :param comment: - :type comment: str - :param description: - :type description: str - :param display_name: - :type display_name: str - :param urn_id: - :type urn_id: str - :param model_name: - :type model_name: str - :param version: - :type version: int - :param type: Possible values include: 'interface', 'capabilityModel' - :type type: str or ~digitaltwinmodelrepositoryservice.models.enum - :param etag: - :type etag: str - :param publisher_id: - :type publisher_id: str - :param publisher_name: - :type publisher_name: str - :param created_on: - :type created_on: datetime - :param updated_on: - :type updated_on: datetime - """ - - _attribute_map = { - 'comment': {'key': 'comment', 'type': 'str'}, - 'description': {'key': 'description', 'type': 'str'}, - 'display_name': {'key': 'displayName', 'type': 'str'}, - 'urn_id': {'key': 'urnId', 'type': 'str'}, - 'model_name': {'key': 'modelName', 'type': 'str'}, - 'version': {'key': 'version', 'type': 'int'}, - 'type': {'key': 'type', 'type': 'str'}, - 'etag': {'key': 'etag', 'type': 'str'}, - 'publisher_id': {'key': 'publisherId', 'type': 'str'}, - 'publisher_name': {'key': 'publisherName', 'type': 'str'}, - 'created_on': {'key': 'createdOn', 'type': 'iso-8601'}, - 'updated_on': {'key': 'updatedOn', 'type': 'iso-8601'}, - } - - def __init__(self, **kwargs): - super(ModelInformation, self).__init__(**kwargs) - self.comment = kwargs.get('comment', None) - self.description = kwargs.get('description', None) - self.display_name = kwargs.get('display_name', None) - self.urn_id = kwargs.get('urn_id', None) - self.model_name = kwargs.get('model_name', None) - self.version = kwargs.get('version', None) - self.type = kwargs.get('type', None) - self.etag = kwargs.get('etag', None) - self.publisher_id = kwargs.get('publisher_id', None) - self.publisher_name = kwargs.get('publisher_name', None) - self.created_on = kwargs.get('created_on', None) - self.updated_on = kwargs.get('updated_on', None) diff --git a/azext_iot/sdk/pnp/models/model_information_py3.py b/azext_iot/sdk/pnp/models/model_information_py3.py deleted file mode 100644 index 4ec8acd27..000000000 --- a/azext_iot/sdk/pnp/models/model_information_py3.py +++ /dev/null @@ -1,72 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ModelInformation(Model): - """ModelInformation. - - :param comment: - :type comment: str - :param description: - :type description: str - :param display_name: - :type display_name: str - :param urn_id: - :type urn_id: str - :param model_name: - :type model_name: str - :param version: - :type version: int - :param type: Possible values include: 'interface', 'capabilityModel' - :type type: str or ~digitaltwinmodelrepositoryservice.models.enum - :param etag: - :type etag: str - :param publisher_id: - :type publisher_id: str - :param publisher_name: - :type publisher_name: str - :param created_on: - :type created_on: datetime - :param updated_on: - :type updated_on: datetime - """ - - _attribute_map = { - 'comment': {'key': 'comment', 'type': 'str'}, - 'description': {'key': 'description', 'type': 'str'}, - 'display_name': {'key': 'displayName', 'type': 'str'}, - 'urn_id': {'key': 'urnId', 'type': 'str'}, - 'model_name': {'key': 'modelName', 'type': 'str'}, - 'version': {'key': 'version', 'type': 'int'}, - 'type': {'key': 'type', 'type': 'str'}, - 'etag': {'key': 'etag', 'type': 'str'}, - 'publisher_id': {'key': 'publisherId', 'type': 'str'}, - 'publisher_name': {'key': 'publisherName', 'type': 'str'}, - 'created_on': {'key': 'createdOn', 'type': 'iso-8601'}, - 'updated_on': {'key': 'updatedOn', 'type': 'iso-8601'}, - } - - def __init__(self, *, comment: str=None, description: str=None, display_name: str=None, urn_id: str=None, model_name: str=None, version: int=None, type=None, etag: str=None, publisher_id: str=None, publisher_name: str=None, created_on=None, updated_on=None, **kwargs) -> None: - super(ModelInformation, self).__init__(**kwargs) - self.comment = comment - self.description = description - self.display_name = display_name - self.urn_id = urn_id - self.model_name = model_name - self.version = version - self.type = type - self.etag = etag - self.publisher_id = publisher_id - self.publisher_name = publisher_name - self.created_on = created_on - self.updated_on = updated_on diff --git a/azext_iot/sdk/pnp/models/search_options.py b/azext_iot/sdk/pnp/models/search_options.py deleted file mode 100644 index 497740e04..000000000 --- a/azext_iot/sdk/pnp/models/search_options.py +++ /dev/null @@ -1,42 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class SearchOptions(Model): - """SearchOptions. - - :param search_keyword: - :type search_keyword: str - :param model_filter_type: Possible values include: 'interface', - 'capabilityModel' - :type model_filter_type: str or - ~digitaltwinmodelrepositoryservice.models.enum - :param continuation_token: - :type continuation_token: str - :param page_size: - :type page_size: int - """ - - _attribute_map = { - 'search_keyword': {'key': 'searchKeyword', 'type': 'str'}, - 'model_filter_type': {'key': 'modelFilterType', 'type': 'str'}, - 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, - 'page_size': {'key': 'pageSize', 'type': 'int'}, - } - - def __init__(self, **kwargs): - super(SearchOptions, self).__init__(**kwargs) - self.search_keyword = kwargs.get('search_keyword', None) - self.model_filter_type = kwargs.get('model_filter_type', None) - self.continuation_token = kwargs.get('continuation_token', None) - self.page_size = kwargs.get('page_size', None) diff --git a/azext_iot/sdk/pnp/models/search_options_py3.py b/azext_iot/sdk/pnp/models/search_options_py3.py deleted file mode 100644 index f6da4fd89..000000000 --- a/azext_iot/sdk/pnp/models/search_options_py3.py +++ /dev/null @@ -1,42 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class SearchOptions(Model): - """SearchOptions. - - :param search_keyword: - :type search_keyword: str - :param model_filter_type: Possible values include: 'interface', - 'capabilityModel' - :type model_filter_type: str or - ~digitaltwinmodelrepositoryservice.models.enum - :param continuation_token: - :type continuation_token: str - :param page_size: - :type page_size: int - """ - - _attribute_map = { - 'search_keyword': {'key': 'searchKeyword', 'type': 'str'}, - 'model_filter_type': {'key': 'modelFilterType', 'type': 'str'}, - 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, - 'page_size': {'key': 'pageSize', 'type': 'int'}, - } - - def __init__(self, *, search_keyword: str=None, model_filter_type=None, continuation_token: str=None, page_size: int=None, **kwargs) -> None: - super(SearchOptions, self).__init__(**kwargs) - self.search_keyword = search_keyword - self.model_filter_type = model_filter_type - self.continuation_token = continuation_token - self.page_size = page_size diff --git a/azext_iot/sdk/pnp/models/search_response_py3.py b/azext_iot/sdk/pnp/models/search_response_py3.py deleted file mode 100644 index 94d05c147..000000000 --- a/azext_iot/sdk/pnp/models/search_response_py3.py +++ /dev/null @@ -1,33 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class SearchResponse(Model): - """SearchResponse. - - :param continuation_token: - :type continuation_token: str - :param results: - :type results: - list[~digitaltwinmodelrepositoryservice.models.ModelInformation] - """ - - _attribute_map = { - 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, - 'results': {'key': 'results', 'type': '[ModelInformation]'}, - } - - def __init__(self, *, continuation_token: str=None, results=None, **kwargs) -> None: - super(SearchResponse, self).__init__(**kwargs) - self.continuation_token = continuation_token - self.results = results diff --git a/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py b/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py index ccce786f6..6334b51a8 100644 --- a/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py +++ b/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py @@ -11,7 +11,7 @@ from functools import partial from uuid import uuid4 from knack.cli import CLIError -from azext_iot.iothub import job_commands as subject +from azext_iot.iothub import commands_job as subject from azext_iot.common.shared import JobStatusType, JobType from ...conftest import build_mock_response, path_service_client, mock_target diff --git a/azext_iot/tests/iothub/pnp_runtime/__init__.py b/azext_iot/tests/iothub/pnp_runtime/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/tests/iothub/pnp_runtime/__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/pnp_runtime/test_iot_pnp_runtime_unit.py b/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py new file mode 100644 index 000000000..11c9e7bf6 --- /dev/null +++ b/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py @@ -0,0 +1,239 @@ +# 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 +import responses +import json +from random import randint +from knack.cli import CLIError +from azext_iot.iothub import commands_pnp_runtime as subject +from ...conftest import mock_target +from ...generators import generate_generic_id + + +device_id = generate_generic_id() +command_id = generate_generic_id() +component_id = generate_generic_id() +generic_result = json.dumps({"result": generate_generic_id()}) + + +class TestPnPRuntimeInvokeCommand(object): + @pytest.fixture(params=[201]) + def service_client_root(self, mocked_response, fixture_ghcs, request): + # Root level device command mock + mocked_response.add( + method=responses.POST, + url="https://{}/digitaltwins/{}/commands/{}".format( + mock_target["entity"], device_id, command_id + ), + body=generic_result, + headers={"x-ms-command-statuscode": str(request.param)}, + status=200, + content_type="application/json", + match_querystring=False, + ) + + mocked_response.metadata = request.param + yield mocked_response + + @pytest.fixture(params=[201]) + def service_client_component(self, mocked_response, fixture_ghcs, request): + # Component level device command mock + mocked_response.add( + method=responses.POST, + url="https://{}/digitaltwins/{}/components/{}/commands/{}".format( + mock_target["entity"], device_id, component_id, command_id + ), + body=generic_result, + headers={"x-ms-command-statuscode": str(request.param)}, + status=200, + content_type="application/json", + match_querystring=False, + ) + + mocked_response.metadata = request.param + yield mocked_response + + @pytest.mark.parametrize( + "request_payload", [("{}"), (json.dumps({"key": str(generate_generic_id())}))], + ) + def test_pnp_runtime_invoke_root_command( + self, fixture_cmd, service_client_root, request_payload + ): + arbitrary_timeout_int = randint(10, 30) + result = subject.invoke_device_command( + cmd=fixture_cmd, + device_id=device_id, + command_name=command_id, + timeout=arbitrary_timeout_int, + payload=request_payload, + hub_name=mock_target["entity"], + ) + + self._assert_common_attributes( + request_payload=request_payload, + executed_client=service_client_root, + result=result, + timeout=arbitrary_timeout_int, + ) + + @pytest.mark.parametrize( + "request_payload", [("{}"), (json.dumps({"key": str(generate_generic_id())}))], + ) + def test_pnp_runtime_invoke_component_command( + self, fixture_cmd, service_client_component, request_payload + ): + arbitrary_timeout_int = randint(10, 30) + result = subject.invoke_device_command( + cmd=fixture_cmd, + device_id=device_id, + command_name=command_id, + timeout=arbitrary_timeout_int, + payload=request_payload, + hub_name=mock_target["entity"], + component_path=component_id, + ) + + self._assert_common_attributes( + request_payload=request_payload, + executed_client=service_client_component, + result=result, + timeout=arbitrary_timeout_int, + ) + + def test_pnp_runtime_invoke_command_error( + self, fixture_cmd, service_client_generic_errors, + ): + with pytest.raises(CLIError): + subject.invoke_device_command( + cmd=fixture_cmd, + device_id=device_id, + command_name=command_id, + timeout=10, + payload=json.dumps({}), + hub_name=mock_target["entity"], + ) + + with pytest.raises(CLIError): + subject.invoke_device_command( + cmd=fixture_cmd, + device_id=device_id, + command_name=command_id, + timeout=10, + payload=json.dumps({}), + hub_name=mock_target["entity"], + component_path=component_id, + ) + + def _assert_common_attributes( + self, request_payload, executed_client, result, timeout=None + ): + assert ( + "connectTimeoutInSeconds={}".format(timeout) + in executed_client.calls[0].request.url + ) + assert ( + "responseTimeoutInSeconds={}".format(timeout) + in executed_client.calls[0].request.url + ) + + assert request_payload == executed_client.calls[0].request.body + assert ( + executed_client.calls[0].request.headers["Content-Type"] + == "application/json; charset=utf-8" + ) + + assert result["payload"] == json.loads(generic_result) + assert result["status"] == str(executed_client.metadata) + + +class TestPnPRuntimeShowDigitalTwin(object): + @pytest.fixture + def service_client(self, mocked_response, fixture_ghcs, request): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}".format(mock_target["entity"], device_id,), + body=generic_result, + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_pnp_runtime_show_digital_twin(self, fixture_cmd, service_client): + result = subject.get_digital_twin( + cmd=fixture_cmd, device_id=device_id, hub_name=mock_target["entity"], + ) + + # Validates simple endpoint behavior. + # The request pattern is validated via responses mock. + assert result == json.loads(generic_result) + + +class TestPnPRuntimeUpdateDigitalTwin(object): + @pytest.fixture + def service_client(self, mocked_response, fixture_ghcs, request): + mocked_response.add( + method=responses.PATCH, + url="https://{}/digitaltwins/{}".format(mock_target["entity"], device_id,), + body=None, + status=202, + content_type="application/json", + match_querystring=False, + ) + + # The command currently will GET a fresh view of the digital twin + # because the update operation does not return anything. + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}".format(mock_target["entity"], device_id,), + body=generic_result, + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "request_json_patch", + [ + ( + '[{"op":"remove", "path":"/thermostat1/targetTemperature"}, ' + '{"op":"add", "path":"/thermostat2/targetTemperature", "value": 22}]' + ), + ('{"op":"add", "path":"/thermostat1/targetTemperature", "value": 54}'), + ], + ) + def test_pnp_runtime_update_digital_twin( + self, fixture_cmd, service_client, request_json_patch + ): + json_patch = json.loads(request_json_patch) + + json_patch_collection = [] + if isinstance(json_patch, dict): + json_patch_collection.append(json_patch) + if isinstance(json_patch, list): + json_patch_collection.extend(json_patch) + + expected_request_body = json.dumps(json_patch_collection) + + result = subject.patch_digital_twin( + cmd=fixture_cmd, + device_id=device_id, + hub_name=mock_target["entity"], + json_patch=request_json_patch, + ) + + # First call for patch + patch_request = service_client.calls[0].request + assert patch_request.body == expected_request_body + assert patch_request.headers["If-Match"] == "*" + + # Result from get twin + assert result == json.loads(generic_result) diff --git a/azext_iot/tests/pnp/__init__.py b/azext_iot/tests/pnp/__init__.py new file mode 100644 index 000000000..f8e676359 --- /dev/null +++ b/azext_iot/tests/pnp/__init__.py @@ -0,0 +1,15 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.testsdk import LiveScenarioTest + + +class PNPLiveScenarioTest(LiveScenarioTest): + def __init__(self, test_scenario): + assert test_scenario + + super(PNPLiveScenarioTest, self).__init__(test_scenario) + PNPLiveScenarioTest.handle = self diff --git a/azext_iot/tests/pnp/test_iot_pnp_int.py b/azext_iot/tests/pnp/test_iot_pnp_int.py new file mode 100644 index 000000000..1bfbfaa49 --- /dev/null +++ b/azext_iot/tests/pnp/test_iot_pnp_int.py @@ -0,0 +1,303 @@ +# 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 +import json +import os + +from io import open +from os.path import exists +from . import PNPLiveScenarioTest +from azext_iot.common.utility import read_file_content +from azext_iot.common.embedded_cli import EmbeddedCLI +from azext_iot.pnp.common import RoleIdentifier +from knack.util import CLIError + +_capability_model_payload = "test_pnp_create_payload_model.json" +_pnp_dns_suffix = "azureiotrepository-test.com" + + +@pytest.mark.usefixtures("set_cwd") +class TestPnPModelLifecycle(PNPLiveScenarioTest): + def __init__(self, _): + super(TestPnPModelLifecycle, self).__init__(_) + account_settings = EmbeddedCLI().invoke("account show").as_json()["user"] + + repo_id = ( + EmbeddedCLI() + .invoke("iot pnp repo list --pnp-dns-suffix {}".format(_pnp_dns_suffix)) + .as_json()[0]["tenantId"] + ) + + self.kwargs.update( + { + "model": "test_model_definition.json", + "user_id": account_settings["name"], + "user_type": account_settings["type"], + "repo_id": repo_id, + "pnp_dns_suffix": _pnp_dns_suffix, + } + ) + + def setUp(self): + if self._testMethodName == "test_model_lifecycle": + + roles = self.cmd( + "iot pnp role-assignment list --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " + "--pnp-dns-suffix {pnp_dns_suffix}" + ) + # check for TenantAdministrator + try: + roles = roles.get_output_in_json() + role_assignments = list( + map(lambda role: role["subject"]["role"], roles) + ) + if RoleIdentifier.tenantAdmin.value not in role_assignments: + self.skipTest("Need TenantAdmin role to perform tests") + except CLIError as e: + self.skipTest(e) + + # Assign roles for model test + + self.cmd( + "iot pnp role-assignment create --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " + "--subject-type {user_type} --role ModelsCreator --pnp-dns-suffix {pnp_dns_suffix}" + ) + + self.cmd( + "iot pnp role-assignment create --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " + "--subject-type {user_type} --role ModelsPublisher --pnp-dns-suffix {pnp_dns_suffix}" + ) + + # Generate model ID + + model = str(read_file_content(_capability_model_payload)) + _model_id = self._generate_model_id(json.loads(model)["@id"]) + self.kwargs.update({"model_id": _model_id}) + model_newContent = model.replace( + json.loads(model)["@id"], self.kwargs["model_id"] + ) + model_newContent = model_newContent.replace("\n", "") + + fo = open(self.kwargs["model"], "w+", encoding="utf-8") + fo.write(model_newContent) + fo.close() + + def tearDown(self): + if exists(self.kwargs["model"]): + os.remove(self.kwargs["model"]) + + if self._testMethodName == "test_model_lifecycle": + + # RBAC for model integration tests (create, show, publish models in tenant) + + self.cmd( + "iot pnp role-assignment delete --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " + "--role ModelsCreator --pnp-dns-suffix {pnp_dns_suffix}" + ) + + self.cmd( + "iot pnp role-assignment delete --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " + "--role ModelsPublisher --pnp-dns-suffix {pnp_dns_suffix}" + ) + + def _generate_model_id(self, model_id): + from datetime import datetime + + now = datetime.now() + date_str = now.strftime("test%m%d%H") + time_str = now.strftime("%M").strip("0") + return "{}:{};{}".format(model_id, date_str, time_str) + + def test_model_lifecycle(self): + + # Error: Invalid model definition file + self.cmd( + "iot pnp model create --model '' --pnp-dns-suffix {pnp_dns_suffix}", + expect_failure=True, + ) + + # Error: wrong path of model definition + self.cmd( + "iot pnp model create --model model.json --pnp-dns-suffix {pnp_dns_suffix}", + expect_failure=True, + ) + + # Success: Create new model + created = self.cmd( + "iot pnp model create --model {model} --pnp-dns-suffix {pnp_dns_suffix}" + ).get_output_in_json() + + assert created["@id"] == self.kwargs["model_id"] + + # Checking the model list + self.cmd( + "iot pnp model list --pnp-dns-suffix {pnp_dns_suffix}", + checks=[ + self.greater_than("length([*])", 0), + self.exists("[?modelId==`{}`]".format(self.kwargs["model_id"])), + ], + ) + + # Get model + model = self.cmd( + "iot pnp model show --model-id {model_id} --pnp-dns-suffix {pnp_dns_suffix}" + ).get_output_in_json() + assert json.dumps(model) + assert model["@id"] == self.kwargs["model_id"] + + # Publish model + published = self.cmd( + "iot pnp model publish --model-id {model_id} --pnp-dns-suffix {pnp_dns_suffix} --yes" + ).get_output_in_json() + assert json.dumps(published) + assert published["@id"] == self.kwargs["model_id"] + + # Checking the model list for published model + self.cmd( + "iot pnp model list -q {model_id} --state Listed --pnp-dns-suffix {pnp_dns_suffix}", + checks=[ + self.greater_than("length([*])", 0), + self.exists("[?modelId==`{}`]".format(self.kwargs["model_id"])), + ], + ) + + +class TestPNPRepo(PNPLiveScenarioTest): + def __init__(self, test_case): + account = EmbeddedCLI().invoke("account show").as_json() + self.user_id = account["user"]["name"] + self.user_type = account["user"]["type"] + super(TestPNPRepo, self).__init__(test_case) + + def setUp(self): + if self._testMethodName == "test_repo_rbac": + # check for TenantAdministrator + try: + repo_id = ( + EmbeddedCLI() + .invoke( + "iot pnp repo list --pnp-dns-suffix {}".format(_pnp_dns_suffix) + ) + .as_json()[0]["tenantId"] + ) + roles = self.cmd( + "iot pnp role-assignment list --resource-id {0} --resource-type Tenant --subject-id {1} " + "--pnp-dns-suffix {2}".format( + repo_id, self.user_id, _pnp_dns_suffix + ) + ) + roles = roles.get_output_in_json() + role_assignments = list( + map(lambda role: role["subject"]["role"], roles) + ) + if RoleIdentifier.tenantAdmin.value not in role_assignments: + self.skipTest("Need TenantAdmin role to perform test") + except CLIError as e: + self.skipTest(e) + + @pytest.mark.skipif(True, reason="Create not functional at the moment") + def test_repo_create(self): + + # create repo + + repo = self.cmd( + "iot pnp repo create --pnp-dns-suffix {pnp_dns_suffix}" + ).get_output_in_json() + + repo_id = repo["tenantId"] + + # list repos + + repos = self.cmd( + "az iot pnp repo list --pnp-dns-suffix {pnp_dns_suffix}" + ).get_output_in_json() + + assert len(repos) == 1 + assert repos[0]["tenantId"] == repo_id + + # get role assignments for repo, should only be one (tenant admin) + + role_assignments = self.cmd( + "az iot pnp role-assignment list --resource-id {0} --resource-type Tenant --pnp-dns-suffix {1}".format( + repo_id, _pnp_dns_suffix + ) + ).get_output_in_json() + + assert len(role_assignments) == 1 + assert role_assignments[0]["subjectMetadata"]["subjectId"] == self.user_id + assert ( + role_assignments[0]["subject"]["role"] == RoleIdentifier.tenantAdmin.value + ) + + def test_repo_rbac(self): + + # get repo + + repos = self.cmd( + "az iot pnp repo list --pnp-dns-suffix {}".format(_pnp_dns_suffix) + ).get_output_in_json() + + repo_id = repos[0]["tenantId"] + + # add role assignment for repo (tenant) + new_role = RoleIdentifier.modelsCreator.value + self.cmd( + "az iot pnp role-assignment create --resource-id {0} --resource-type Tenant " + "--subject-id {1} --subject-type {2} --role {3} --pnp-dns-suffix {4}".format( + repo_id, self.user_id, self.user_type, new_role, _pnp_dns_suffix + ) + ) + + # get newest role assignments for user + + role_assignments = self.cmd( + "az iot pnp role-assignment list --resource-id {0} --resource-type Tenant --subject-id {1} " + "--pnp-dns-suffix {2}".format(repo_id, self.user_id, _pnp_dns_suffix) + ).get_output_in_json() + + # ensure our new role exists + + assert ( + len( + [ + role + for role in role_assignments + if role["subjectMetadata"]["subjectId"] == self.user_id + and role["subject"]["role"] == new_role + ] + ) + == 1 + ) + + # delete role assignment + + self.cmd( + "az iot pnp role-assignment delete --resource-id {0} --resource-type Tenant --role {1} --subject {2} " + "--pnp-dns-suffix {3}".format( + repo_id, new_role, self.user_id, _pnp_dns_suffix + ) + ) + + # get assignments again + + role_assignments = self.cmd( + "az iot pnp role-assignment list --resource-id {0} --resource-type Tenant --subject-id {1} " + "--pnp-dns-suffix {2}".format(repo_id, self.user_id, _pnp_dns_suffix) + ).get_output_in_json() + + # ensure our new role does not exist + assert ( + len( + [ + role + for role in role_assignments + if role["subjectMetadata"]["subjectId"] == self.user_id + and role["subject"]["role"] == new_role + ] + ) + == 0 + ) diff --git a/azext_iot/tests/pnp/test_iot_pnp_unit.py b/azext_iot/tests/pnp/test_iot_pnp_unit.py new file mode 100644 index 000000000..d82872063 --- /dev/null +++ b/azext_iot/tests/pnp/test_iot_pnp_unit.py @@ -0,0 +1,515 @@ +# 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 +import json + +from azext_iot.pnp import commands_api as dataplane, commands_repository as repo +from azext_iot.pnp.common import RoleIdentifier +from azext_iot.common.utility import url_encode_str, read_file_content +from knack.util import CLIError +from ..conftest import fixture_cmd, path_service_client, build_mock_response + + +_pnp_create_model_payload_file = "test_pnp_create_payload_model.json" +_pnp_generic_model_id = "urn:example:capabilityModels:Mxchip:1" + +mock_target = {} + + +def generate_pnp_model_create_payload(content_from_file=False): + if content_from_file: + return (None, _pnp_create_model_payload_file) + + return ( + str(read_file_content(_pnp_create_model_payload_file)), + _pnp_create_model_payload_file, + ) + + +@pytest.mark.usefixtures("set_cwd") +class TestModelRepoModelCreate(object): + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, request, set_cwd): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.mark.parametrize( + "content_from_file", [True, False], + ) + def test_model_create(self, fixture_cmd, serviceclient, content_from_file, set_cwd): + + payload = None + payload_scenario = generate_pnp_model_create_payload(content_from_file) + # If file path provided + if not payload_scenario[0]: + payload = payload_scenario[1] + payload = str(read_file_content(_pnp_create_model_payload_file)) + + model_id = json.loads(payload)["@id"] + dataplane.iot_pnp_model_create( + cmd=fixture_cmd, model=payload, + ) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + data = args[0][0].data + + assert method == "PUT" + assert "/models/{}?".format(url_encode_str(model_id, plus=True)) in url + assert json.dumps(data) + + @pytest.mark.parametrize( + "content_from_file", [True, False], + ) + def test_model_create_error( + self, fixture_cmd, serviceclient_generic_error, content_from_file + ): + payload = None + payload_scenario = generate_pnp_model_create_payload(content_from_file) + if not payload_scenario[0]: + payload = payload_scenario[1] + payload = str(read_file_content(_pnp_create_model_payload_file)) + + payload = json.loads(payload) + del payload["@id"] + payload = json.dumps(payload) + with pytest.raises(CLIError): + dataplane.iot_pnp_model_create( + fixture_cmd, model=payload, + ) + + @pytest.mark.parametrize("model_id, exp", [("", CLIError)]) + def test_model_create_invalid_args(self, serviceclient, model_id, exp): + with pytest.raises(exp): + dataplane.iot_pnp_model_create(fixture_cmd, model="{{}") + + def test_model_create_invalid_payload(self, serviceclient): + + payload = str(read_file_content(_pnp_create_model_payload_file)) + payload = json.loads(payload) + del payload["@id"] + payload = json.dumps(payload) + with pytest.raises(CLIError): + dataplane.iot_pnp_model_create(fixture_cmd, model=payload) + + +class TestModelRepoModelPublish(object): + @pytest.fixture(params=[400, 418, 503]) + def serviceclient_publish_errors(self, mocker, fixture_sas, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response( + mocker, request.param, {"error": "something failed"} + ) + return service_client + + @pytest.fixture(params=[(200, 200, 201), (200, 200, 204), (200, 200, 412)]) + def serviceclient(self, mocker, request): + service_client = mocker.patch(path_service_client) + payload_list = { + "continuationToken": "null", + "results": [ + { + "comment": "", + "createdOn": "2019-07-09T07:46:06.044161+00:00", + "description": "", + "displayName": "Mxchip 1", + "etag": '"41006e67-0000-0800-0000-5d2501b80000"', + "modelName": "example:capabilityModels:Mxchip", + "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", + "publisherName": "microsoft.com", + "type": "CapabilityModel", + "updatedOn": "2019-07-09T21:06:00.072063+00:00", + "urnId": "urn:example:capabilityModels:Mxchip:1", + "version": 1, + } + ], + } + payload_show = { + "@id": "urn:example:capabilityModels:Mxchip:1", + "@type": "CapabilityModel", + "displayName": "Mxchip 1", + "implements": [ + {"schema": "urn:example:interfaces:MXChip:1", "name": "MXChip1"} + ], + "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json", + } + test_side_effect = [ + build_mock_response( + mocker, request.param[0], payload=payload_list, headers={"eTag": "eTag"} + ), + build_mock_response( + mocker, request.param[1], payload=payload_show, headers={"eTag": "eTag"} + ), + build_mock_response(mocker, request.param[2], {}, headers={"eTag": "eTag"}), + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) + def test_model_publish(self, fixture_cmd, serviceclient, target_model): + dataplane.iot_pnp_model_publish( + fixture_cmd, model_id=target_model, + ) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + headers = args[0][0].headers + + assert method == "PUT" + assert "/models/{}?".format(url_encode_str(target_model, plus=True)) in url + assert "update-metadata=true" in url + assert headers.get("x-ms-model-state") == "Listed" + + @pytest.mark.parametrize("target_model", [("acv.17")]) + def test_model_publish_error( + self, fixture_cmd, serviceclient_publish_errors, target_model + ): + with pytest.raises(CLIError): + dataplane.iot_pnp_model_publish( + fixture_cmd, model_id=target_model, + ) + + +class TestModelRepoModelShow(object): + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, request): + service_client = mocker.patch(path_service_client) + payload = { + "@id": "urn:example:capabilityModels:Mxchip:1", + "@type": "CapabilityModel", + "displayName": "Mxchip 1", + "implements": [ + {"schema": "urn:example:interfaces:MXChip:1", "name": "MXChip1"} + ], + "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json", + } + service_client.return_value = build_mock_response( + mocker, request.param, payload=payload + ) + return service_client + + @pytest.fixture(params=[404, 500]) + def serviceclient_generic_error(self, mocker, fixture_sas, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response( + mocker, request.param, {"error": "something failed"} + ) + return service_client + + @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) + def test_model_show(self, fixture_cmd, serviceclient, target_model): + result = dataplane.iot_pnp_model_show(fixture_cmd, model_id=target_model) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + + assert method == "GET" + assert "/models/{}?".format(url_encode_str(target_model, plus=True)) in url + assert json.dumps(result) + + @pytest.mark.parametrize("target_model", [("acv:17")]) + def test_model_show_error( + self, fixture_cmd, serviceclient_generic_error, target_model + ): + response = dataplane.iot_pnp_model_show(fixture_cmd, model_id=target_model) + assert "error" in response + + @pytest.fixture(params=[400]) + def serviceclientemptyresult(self, mocker, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) + def test_model_show_no_result( + self, fixture_cmd, serviceclientemptyresult, target_model + ): + with pytest.raises(CLIError): + dataplane.iot_pnp_model_show( + fixture_cmd, model_id=target_model, + ) + + +class TestModelRepoModelList(object): + @pytest.fixture(params=[200]) + def service_client(self, mocker, request): + serviceclient = mocker.patch(path_service_client) + payload = json.dumps( + [ + { + "_etag": '"00000000-0000-0800-0000-000000000000"', + "comment": "", + "countOfCommands": 0, + "countOfComponents": 0, + "countOfExtends": 0, + "countOfProperties": 0, + "countOfRelationships": 0, + "countOfSchemas": 0, + "countOfTelemetries": 0, + "createdBy": "user@contoso.com", + "createdDate": "2020-04-29T17:17:44.3055434+00:00", + "displayName": "Device Information", + "modelId": "test:model:name;1", + "modelName": "test:model:name", + "modelState": "Listed", + "modelType": "Interface", + "publisherId": "00000000-0000-0000-0000-000000000000", + "publisherName": "user@contoso.com", + "updatedDate": "2020-04-29T17:24:48.8645696+00:00", + "version": "1", + } + ] + ) + serviceclient.return_value = build_mock_response(mocker, request.param, payload) + return serviceclient + + @pytest.fixture(params=[400, 503]) + def serviceclient_generic_error(self, mocker, fixture_sas, request): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response( + mocker, request.param, {"error": "something failed"} + ) + return service_client + + @pytest.mark.parametrize( + ["search_content", "shared"], + [ + ( + { + "searchKeyword": "keyword", + "modelType": "type", + "modelState": "state", + "publisherId": "publisher", + "createdBy": "creator", + }, + False, + ), + ({"searchKeyword": "test"}, True), + ({}, False), + ], + ) + def test_model_list(self, fixture_cmd, service_client, search_content, shared): + result = dataplane.iot_pnp_model_list( + fixture_cmd, + keyword=search_content.get("searchKeyword"), + model_type=search_content.get("modelType"), + model_state=search_content.get("modelState"), + publisher_id=search_content.get("publisherId"), + created_by=search_content.get("createdBy"), + shared=shared, + ) + args = service_client.call_args + url = args[0][0].url + method = args[0][0].method + headers = args[0][0].headers + body = json.loads(args[0][0].body) + + for k in search_content.keys(): + assert body[k] == search_content[k] + + assert ( + headers.get("x-ms-show-shared-models-only") == "true" if shared else "false" + ) + assert method == "POST" + + assert "/models/search?" in url + assert len(result) == 1 + + def test_model_list_error(self, fixture_cmd, serviceclient_generic_error): + with pytest.raises(CLIError): + dataplane.iot_pnp_model_list(fixture_cmd,) + + +class TestModelRepoRepoCreate(object): + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, request, set_cwd): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + def test_repo_create(self, fixture_cmd, serviceclient): + repo.iot_pnp_tenant_create(fixture_cmd) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + + assert "/tenants" in url + assert method == "PUT" + + +class TestModelRepoRepoList(object): + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, request, set_cwd): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, [{}]) + return service_client + + def test_repo_list(self, fixture_cmd, serviceclient): + repo.iot_pnp_tenant_show(fixture_cmd) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + + assert "/tenants" in url + assert method == "GET" + + +class TestModelRepoRBAC(object): + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, request, set_cwd): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, {}) + return service_client + + @pytest.fixture(params=[200]) + def serviceclient_arr(self, mocker, request, set_cwd): + service_client = mocker.patch(path_service_client) + service_client.return_value = build_mock_response(mocker, request.param, [{}]) + return service_client + + @pytest.mark.parametrize( + ["role", "resource_id", "resource_type", "subject_id", "subject_type"], + [ + ( + RoleIdentifier.modelAdmin, + "12345-12345-12345-12345", + "Tenant", + "user@tenant.com", + "user", + ), + ( + RoleIdentifier.modelReader, + "abc:123:model;2", + "Model", + "user@tenant.com", + "user", + ), + ( + RoleIdentifier.modelsCreator, + "12345-12345-12345-12345", + "Tenant", + "12345-12345-12345", + "ServicePrincipal", + ), + ( + RoleIdentifier.modelsPublisher, + "abc:123:model;2", + "Model", + "12345-12345-12345", + "ServicePrincipal", + ), + ], + ) + def test_repo_role_create( + self, + fixture_cmd, + serviceclient, + role, + resource_id, + resource_type, + subject_id, + subject_type, + ): + + resource_id = "resource_id" + resource_type = "resource_type" + subject_id = "subject" + subject_type = "subject_type" + + repo.iot_pnp_role_create( + fixture_cmd, resource_id, resource_type, subject_id, subject_type, role + ) + args = serviceclient.call_args + url = args[0][0].url + method = args[0][0].method + data = json.loads(args[0][0].data) + + assert method == "PUT" + assert ( + "/resources/{0}/types/{1}/subjects/{2}".format( + resource_id, resource_type, subject_id + ) + in url + ) + assert data["subjectType"] == subject_type + assert data["role"] == role.value + assert data["resourceType"] == resource_type + + @pytest.mark.parametrize( + ["resource_id", "resource_type", "subject_id"], + [ + ("12345-12345-12345", "Tenant", "user@tenant.com"), + ("test:model:perms;1", "Model", "12345-12345-12345"), + ("12345-12345-12345", "Tenant", None), + ("test:model:perms;1", "Model", None), + ], + ) + def test_repo_role_show( + self, fixture_cmd, serviceclient_arr, resource_id, resource_type, subject_id + ): + + repo.iot_pnp_role_list(fixture_cmd, resource_id, resource_type, subject_id) + args = serviceclient_arr.call_args + url = args[0][0].url + method = args[0][0].method + + assert method == "GET" + assert ( + ( + "/resources/{0}/types/{1}/subjects/{2}".format( + url_encode_str(resource_id), + resource_type, + url_encode_str(subject_id), + ) + in url + ) + if subject_id + else ( + "/resources/{0}/types/{1}".format( + url_encode_str(resource_id), resource_type + ) + in url + ) + ) + + @pytest.mark.parametrize( + ["role", "resource_id", "resource_type", "subject_id"], + [ + ( + RoleIdentifier.modelAdmin, + "12345-12345-12345-12345", + "Tenant", + "user@tenant.com", + ), + ( + RoleIdentifier.modelReader, + "abc:123:model;2", + "Model", + "12345-12345-12345", + ), + ], + ) + def test_repo_role_delete( + self, fixture_cmd, serviceclient, role, resource_id, resource_type, subject_id, + ): + + repo.iot_pnp_role_delete( + fixture_cmd, resource_id, resource_type, role, subject_id + ) + args = serviceclient.call_args + url = args[0][0].url + + assert ( + "/resources/{0}/types/{1}/subjects/{2}/roles/{3}".format( + url_encode_str(resource_id), + resource_type, + url_encode_str(subject_id), + role.value, + ) + in url + ) diff --git a/azext_iot/tests/test_pnp_create_payload_interface.json b/azext_iot/tests/pnp/test_pnp_create_payload_interface.json similarity index 100% rename from azext_iot/tests/test_pnp_create_payload_interface.json rename to azext_iot/tests/pnp/test_pnp_create_payload_interface.json diff --git a/azext_iot/tests/pnp/test_pnp_create_payload_model.json b/azext_iot/tests/pnp/test_pnp_create_payload_model.json new file mode 100644 index 000000000..a5bb86ca5 --- /dev/null +++ b/azext_iot/tests/pnp/test_pnp_create_payload_model.json @@ -0,0 +1,13 @@ +{ + "@id": "dtmi:iotpnpcli", + "@type": "Interface", + "displayName": "My Sample Root Interface", + "contents": [ + { + "@type": "Component", + "schema": "dtmi:Contoso1:EnvironmentalSensor;1", + "name": "sensor" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/azext_iot/tests/test_pnp_interface_show.json b/azext_iot/tests/pnp/test_pnp_interface_show.json similarity index 100% rename from azext_iot/tests/test_pnp_interface_show.json rename to azext_iot/tests/pnp/test_pnp_interface_show.json diff --git a/azext_iot/tests/test_iot_digitaltwin_unit.py b/azext_iot/tests/test_iot_digitaltwin_unit.py deleted file mode 100644 index e72878431..000000000 --- a/azext_iot/tests/test_iot_digitaltwin_unit.py +++ /dev/null @@ -1,611 +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. -# -------------------------------------------------------------------------------------------- - -# TODO: This module will look very different for the PnP refresh. - -import pytest -import json -import os - -from azext_iot.operations import digitaltwin as subject -from azext_iot.operations.digitaltwin import INTERFACE_KEY_NAME -from azext_iot.constants import PNP_ENDPOINT -from azext_iot.tests.conftest import build_mock_response -from azext_iot.tests.generators import create_req_monitor_events -from knack.util import CLIError -from azext_iot.common.utility import read_file_content - -_device_digitaltwin_invoke_command_payload = ( - "test_device_digitaltwin_invoke_command.json" -) -_device_digitaltwin_payload_file = "test_device_digitaltwin_interfaces.json" -_device_digitaltwin_property_update_payload_file = ( - "test_device_digitaltwin_property_update.json" -) -_pnp_show_interface_file = "test_pnp_interface_show.json" -_pnp_list_interface_file = "test_pnp_interface_list.json" -path_iot_hub_monitor_events_entrypoint = ( - "azext_iot.operations.digitaltwin._iot_hub_monitor_events" -) -device_id = "mydevice" -hub_entity = "myhub.azure-devices.net" - -# Patch Paths # -path_mqtt_client = "azext_iot.operations._mqtt.mqtt.Client" -path_service_client = "msrest.service_client.ServiceClient.send" -path_ghcs = "azext_iot.iothub.providers.discovery.IotHubDiscovery.get_target" -path_pnpcs = "azext_iot.operations.pnp.get_iot_pnp_connection_string" -path_sas = "azext_iot._factory.SasTokenAuthentication" - -mock_target = {} -mock_target["entity"] = hub_entity -mock_target["primarykey"] = "rJx/6rJ6rmG4ak890+eW5MYGH+A0uzRvjGNjg3Ve8sfo=" -mock_target["secondarykey"] = "aCd/6rJ6rmG4ak890+eW5MYGH+A0uzRvjGNjg3Ve8sfo=" -mock_target["policy"] = "iothubowner" -mock_target["subscription"] = "5952cff8-bcd1-4235-9554-af2c0348bf23" -mock_target["location"] = "westus2" -mock_target["sku_tier"] = "Standard" -generic_cs_template = "HostName={};SharedAccessKeyName={};SharedAccessKey={}" - -mock_pnptarget = {} -mock_pnptarget["entity"] = PNP_ENDPOINT -mock_pnptarget["primarykey"] = "rJx/6rJ6rmG4ak890+eW5MYGH+A0uzRvjGNjg3Ve8sfo=" -mock_pnptarget["repository_id"] = "1b10ab39a62946ffada85db3b785b3dd" -mock_pnptarget["policy"] = "43325732479e453093a3d1ae5b95c62e" -generic_pnpcs_template = ( - "HostName={};RepositoryId={};SharedAccessKeyName={};SharedAccessKey={}" -) - - -def generate_pnpcs( - pnp=PNP_ENDPOINT, - repository=mock_pnptarget["repository_id"], - policy=mock_target["policy"], - key=mock_target["primarykey"], -): - return generic_pnpcs_template.format(pnp, repository, policy, key) - - -def generate_cs( - hub=hub_entity, policy=mock_target["policy"], key=mock_target["primarykey"] -): - return generic_cs_template.format(hub, policy, key) - - -mock_pnptarget["cs"] = generate_pnpcs() -mock_target["cs"] = generate_cs() - - -def change_dir(): - from inspect import getsourcefile - - os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) - - -def generate_device_interfaces_payload(): - change_dir() - return json.loads(read_file_content(_device_digitaltwin_payload_file)) - - -def generate_pnp_interface_show_payload(): - change_dir() - return json.loads(read_file_content(_pnp_show_interface_file)) - - -def generate_pnp_interface_list_payload(): - change_dir() - return json.loads(read_file_content(_pnp_list_interface_file)) - - -def generate_device_digitaltwin_property_update_payload(content_from_file=False): - change_dir() - if content_from_file: - return (None, _device_digitaltwin_property_update_payload_file) - - return ( - str(read_file_content(_device_digitaltwin_property_update_payload_file)), - _device_digitaltwin_property_update_payload_file, - ) - - -def generate_device_digitaltwin_invoke_command_payload(content_from_file=False): - change_dir() - if content_from_file: - return (None, _device_digitaltwin_invoke_command_payload) - - return ( - str(read_file_content(_device_digitaltwin_invoke_command_payload)), - _device_digitaltwin_invoke_command_payload, - ) - - -@pytest.fixture() -def fixture_ghcs(mocker): - ghcs = mocker.patch(path_ghcs) - ghcs.return_value = mock_target - return ghcs - - -@pytest.fixture() -def fixture_pnpcs(mocker): - pnpcs = mocker.patch(path_pnpcs) - pnpcs.return_value = mock_pnptarget - return pnpcs - - -@pytest.fixture() -def fixture_monitor_events_entrypoint(mocker): - return mocker.patch(path_iot_hub_monitor_events_entrypoint) - - -class TestDTInterfaceList(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, request): - service_client = mocker.patch(path_service_client) - output = generate_device_interfaces_payload() - service_client.return_value = build_mock_response(mocker, request.param, output) - return service_client - - def test_iot_digitaltwin_interface_list(self, fixture_cmd, serviceclient): - result = subject.iot_digitaltwin_interface_list( - fixture_cmd, device_id=device_id, login=mock_target["cs"] - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - - assert method == "GET" - assert ( - "{}/digitalTwins/{}/interfaces/{}?".format( - mock_target["entity"], device_id, INTERFACE_KEY_NAME - ) - in url - ) - assert json.dumps(result) - assert len(result["interfaces"]) == 3 - - @pytest.mark.parametrize("exp", [(CLIError)]) - def test_iot_digitaltwin_interface_list_error( - self, fixture_cmd, serviceclient_generic_error, exp - ): - with pytest.raises(exp): - subject.iot_digitaltwin_interface_list( - fixture_cmd, device_id=device_id, login=mock_target["cs"] - ) - - -class TestDTCommandList(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - output = generate_device_interfaces_payload() - payload_list = generate_pnp_interface_list_payload() - payload_show = generate_pnp_interface_show_payload() - test_side_effect = [ - build_mock_response(mocker, request.param, payload=output), - build_mock_response(mocker, request.param, payload=payload_list), - build_mock_response(mocker, request.param, payload=payload_show), - ] - service_client.side_effect = test_side_effect - return service_client - - def test_iot_digitaltwin_command_list(self, fixture_cmd, serviceclient): - result = subject.iot_digitaltwin_command_list( - fixture_cmd, - device_id=device_id, - source_model="public", - interface="environmentalSensor", - login=mock_target["cs"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - - assert method == "GET" - assert "/models/" in url - assert json.dumps(result) - assert len(result["interfaces"]) == 1 - assert result["interfaces"][0]["name"] == "environmentalSensor" - assert len(result["interfaces"][0]["commands"]) == 3 - - @pytest.mark.parametrize("exp", [(CLIError)]) - def test_iot_digitaltwin_command_list_error( - self, fixture_cmd, serviceclient_generic_error, exp - ): - with pytest.raises(exp): - subject.iot_digitaltwin_command_list( - fixture_cmd, - device_id=device_id, - source_model="public", - login=mock_target["cs"], - ) - - @pytest.mark.parametrize("interface, exp", [("inter1", CLIError)]) - def test_iot_digitaltwin_command_list_args_error( - self, fixture_cmd, serviceclient, interface, exp - ): - with pytest.raises(exp): - subject.iot_digitaltwin_command_list( - fixture_cmd, - device_id=device_id, - interface=interface, - login=mock_target["cs"], - source_model="public", - ) - - -class TestDTPropertiesList(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - output = generate_device_interfaces_payload() - payload_list = generate_pnp_interface_list_payload() - payload_show = generate_pnp_interface_show_payload() - test_side_effect = [ - build_mock_response(mocker, request.param, payload=output), - build_mock_response(mocker, request.param, payload=payload_list), - build_mock_response(mocker, request.param, payload=payload_show), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize("target_interface", [("environmentalSensor")]) - def test_iot_digitaltwin_properties_list( - self, fixture_cmd, serviceclient, target_interface - ): - result = subject.iot_digitaltwin_properties_list( - fixture_cmd, - device_id=device_id, - source_model="public", - interface=target_interface, - login=mock_target["cs"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - - assert method == "GET" - assert "/models/" in url - assert json.dumps(result) - if target_interface: - assert len(result["interfaces"]) == 1 - assert result["interfaces"][0]["name"] == "environmentalSensor" - assert len(result["interfaces"][0]["properties"]) == 3 - - @pytest.mark.parametrize("exp", [(CLIError)]) - def test_iot_digitaltwin_properties_list_error( - self, fixture_cmd, serviceclient_generic_error, exp - ): - with pytest.raises(exp): - subject.iot_digitaltwin_properties_list( - fixture_cmd, - device_id=device_id, - source_model="public", - login=mock_target["cs"], - ) - - @pytest.mark.parametrize("interface, exp", [("inter1", CLIError)]) - def test_iot_digitaltwin_properties_list_args_error( - self, fixture_cmd, serviceclient, interface, exp - ): - with pytest.raises(exp): - subject.iot_digitaltwin_properties_list( - fixture_cmd, - device_id=device_id, - interface=interface, - login=mock_target["cs"], - source_model="public", - ) - - -class TestDTPropertyUpdate(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.mark.parametrize( - "payload_scenario", - [ - (generate_device_digitaltwin_property_update_payload()), - ( - generate_device_digitaltwin_property_update_payload( - content_from_file=True - ) - ), - ], - ) - def test_iot_digitaltwin_property_update( - self, fixture_cmd, serviceclient, payload_scenario - ): - - payload = None - - # If file path provided - if not payload_scenario[0]: - payload = payload_scenario[1] - else: - payload = str( - read_file_content(_device_digitaltwin_property_update_payload_file) - ) - - subject.iot_digitaltwin_property_update( - fixture_cmd, - device_id=device_id, - interface_payload=payload, - login=mock_target["cs"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - - assert method == "PATCH" - assert ( - "{}/digitalTwins/{}/interfaces?".format(mock_target["entity"], device_id) - in url - ) - - @pytest.mark.parametrize( - "payload_scenario", [(generate_device_digitaltwin_property_update_payload())] - ) - def test_iot_digitaltwin_property_update_error( - self, fixture_cmd, serviceclient_generic_error, payload_scenario - ): - with pytest.raises(CLIError): - subject.iot_digitaltwin_property_update( - fixture_cmd, - device_id=device_id, - interface_payload=payload_scenario[0], - login=mock_target["cs"], - ) - - -class TestDTInvokeCommand(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - output = generate_device_interfaces_payload() - payload_list = generate_pnp_interface_list_payload() - payload_show = generate_pnp_interface_show_payload() - test_side_effect = [ - build_mock_response(mocker, request.param, payload=output), - build_mock_response(mocker, request.param, payload=payload_list), - build_mock_response(mocker, request.param, payload=payload_show), - build_mock_response(mocker, request.param, {}), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize( - "command_payload", - [ - (generate_device_digitaltwin_invoke_command_payload()), - ( - generate_device_digitaltwin_invoke_command_payload( - content_from_file=True - ) - ), - ], - ) - def test_iot_digitaltwin_invoke_command( - self, fixture_cmd, serviceclient, command_payload - ): - - payload = None - interface = "environmentalSensor" - command = "blink" - - # If file path provided - if not command_payload[0]: - payload = command_payload[1] - else: - payload = str(read_file_content(_device_digitaltwin_invoke_command_payload)) - - subject.iot_digitaltwin_invoke_command( - fixture_cmd, - device_id=device_id, - interface=interface, - command_name=command, - command_payload=payload, - login=mock_target["cs"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - - assert method == "POST" - assert ( - "{}/digitalTwins/{}/interfaces/{}/commands/{}?".format( - mock_target["entity"], device_id, interface, command - ) - in url - ) - - @pytest.fixture(params=[(200, 400), (200, 401), (200, 500)]) - def serviceclient_error(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - output = generate_device_interfaces_payload() - test_side_effect = [ - build_mock_response(mocker, request.param[0], payload=output), - build_mock_response(mocker, request.param[1], {}), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize( - "command_payload, interface, command", - [ - ( - generate_device_digitaltwin_invoke_command_payload(), - "environmentalSensor", - "blink", - ), - ( - generate_device_digitaltwin_invoke_command_payload( - content_from_file=True - ), - "environmentalSensor", - "blink", - ), - (generate_device_digitaltwin_invoke_command_payload(), "test", "blink"), - ( - generate_device_digitaltwin_invoke_command_payload(), - "environmentalSensor", - "test", - ), - ], - ) - def test_iot_digitaltwin_property_update_error( - self, fixture_cmd, serviceclient_error, command_payload, interface, command - ): - payload = None - if not command_payload[0]: - payload = command_payload[1] - else: - payload = str(read_file_content(_device_digitaltwin_invoke_command_payload)) - with pytest.raises(CLIError): - subject.iot_digitaltwin_invoke_command( - fixture_cmd, - device_id=device_id, - interface=interface, - command_name=command, - command_payload=payload, - login=mock_target["cs"], - ) - - -class TestDTMonitorEvents(object): - @pytest.mark.parametrize( - "req", - [ - ( - create_req_monitor_events( - device_id=device_id, - interface_name="environmentalSensor", - yes=True, - properties="all", - consumer_group="group1", - login=mock_target["cs"], - ) - ), - ( - create_req_monitor_events( - device_id=device_id, - interface_name="environmentalSensor", - yes=False, - properties="sys anno", - login=mock_target["cs"], - ) - ), - ( - create_req_monitor_events( - device_id=device_id, - interface_name="environmentalSensor", - enqueued_time="5432154321", - yes=True, - properties="sys", - login=mock_target["cs"], - ) - ), - ( - create_req_monitor_events( - device_id=device_id, - enqueued_time="321321321", - content_type="application/json", - timeout=100, - yes=True, - properties="all", - login=mock_target["cs"], - ) - ), - ( - create_req_monitor_events( - device_id=device_id, - timeout=100, - yes=True, - properties="all", - hub_name="myhub", - resource_group_name="myrg", - ) - ), - ( - create_req_monitor_events( - device_id=device_id, - yes=True, - properties="all", - login=mock_target["cs"], - ) - ), - ( - create_req_monitor_events( - device_id=device_id, repair=True, login=mock_target["cs"] - ) - ), - (create_req_monitor_events(login=mock_target["cs"])), - (create_req_monitor_events(hub_name="myhub")), - ( - create_req_monitor_events( - device_query="select * from devices", login=mock_target["cs"] - ) - ), - ( - create_req_monitor_events( - interface_name="environmentalSensor", hub_name="myiothub" - ) - ), - ( - create_req_monitor_events( - device_id="auxFridgeUnit-*", - device_query="select * from devices", - interface_name="environmentalSensor", - login=mock_target["cs"], - ) - ), - ], - ) - def test_iot_digitaltwin_monitor_events_entrypoint( - self, fixture_cmd, fixture_monitor_events_entrypoint, req - ): - subject.iot_digitaltwin_monitor_events( - fixture_cmd, - device_id=req["device_id"], - device_query=req["device_query"], - interface=req["interface_name"], - consumer_group=req["consumer_group"], - content_type=req["content_type"], - enqueued_time=req["enqueued_time"], - timeout=req["timeout"], - hub_name=req["hub_name"], - resource_group_name=req["resource_group_name"], - yes=req["yes"], - properties=req["properties"], - repair=req["repair"], - login=req["login"], - ) - - monitor_events_args = fixture_monitor_events_entrypoint.call_args[1] - - dt_attribute_set = [ - "device_id", - "device_query", - "interface_name", - "consumer_group", - "enqueued_time", - "content_type", - "timeout", - "login", - "hub_name", - "resource_group_name", - "yes", - "properties", - "repair", - ] - for attribute in dt_attribute_set: - if req[attribute]: - assert monitor_events_args[attribute] == req[attribute] - else: - assert not monitor_events_args[attribute] diff --git a/azext_iot/tests/test_iot_pnp_int.py b/azext_iot/tests/test_iot_pnp_int.py deleted file mode 100644 index 2fd27488b..000000000 --- a/azext_iot/tests/test_iot_pnp_int.py +++ /dev/null @@ -1,237 +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. -# -------------------------------------------------------------------------------------------- - -import random -import json -import os - -from io import open -from os.path import exists -from azure.cli.testsdk import LiveScenarioTest -from azext_iot.common.utility import read_file_content - - -# Set these to the proper PnP Endpoint, PnP Cstring and PnP Repository for Live Integration Tests. -_endpoint = os.environ.get("azext_pnp_endpoint") -_repo_id = os.environ.get("azext_pnp_repository") -_repo_cs = os.environ.get("azext_pnp_cs") - -_interface_payload = "test_pnp_create_payload_interface.json" -_capability_model_payload = "test_pnp_create_payload_model.json" - -if not all([_endpoint, _repo_id, _repo_cs]): - raise ValueError( - "Set azext_pnp_endpoint, azext_pnp_repository and azext_pnp_cs to run PnP model integration tests." - ) - - -def change_dir(): - from inspect import getsourcefile - - os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) - - -class TestPnPModel(LiveScenarioTest): - - rand_val = random.randint(1, 10001) - - def __init__(self, _): - super(TestPnPModel, self).__init__(_) - self.kwargs.update( - { - "endpoint": _endpoint, - "repo": _repo_id, - "repo_cs": _repo_cs, - "interface": "test_interface_definition.json", - "interface_updated": "test_interface_updated_definition.json", - "model": "test_model_definition.json", - "model_updated": "test_model_updated_definition.json", - } - ) - - def setUp(self): - change_dir() - if self._testMethodName == "test_interface_life_cycle": - interface = str(read_file_content(_interface_payload)) - _interface_id = "{}{}".format( - json.loads(interface)["@id"], TestPnPModel.rand_val - ) - self.kwargs.update({"interface_id": _interface_id}) - interface_newContent = interface.replace( - json.loads(interface)["@id"], self.kwargs["interface_id"] - ) - interface_newContent = interface_newContent.replace("\n", "") - - fo = open(self.kwargs["interface"], "w+", encoding="utf-8") - fo.write(interface_newContent) - fo.close() - - if self._testMethodName == "test_model_life_cycle": - model = str(read_file_content(_capability_model_payload)) - _model_id = "{}{}".format(json.loads(model)["@id"], TestPnPModel.rand_val) - self.kwargs.update({"model_id": _model_id}) - model_newContent = model.replace( - json.loads(model)["@id"], self.kwargs["model_id"] - ) - model_newContent = model_newContent.replace("\n", "") - - fo = open(self.kwargs["model"], "w+", encoding="utf-8") - fo.write(model_newContent) - fo.close() - - def tearDown(self): - change_dir() - if exists(self.kwargs["interface_updated"]): - os.remove(self.kwargs["interface_updated"]) - if exists(self.kwargs["model_updated"]): - os.remove(self.kwargs["model_updated"]) - if exists(self.kwargs["interface"]): - os.remove(self.kwargs["interface"]) - if exists(self.kwargs["model"]): - os.remove(self.kwargs["model"]) - - def test_interface_life_cycle(self): - - # Error: missing repo-id or login - self.cmd( - "iot pnp interface create -e {endpoint} --def {interface}", - expect_failure=True, - ) - - # Error: Invalid Interface definition file - self.cmd( - "iot pnp interface create -e {endpoint} -r {repo} --def interface", - expect_failure=True, - ) - - # Error: wrong path of Interface definition - self.cmd( - "iot pnp interface create -e {endpoint} -r {repo} --def interface.json", - expect_failure=True, - ) - - # Success: Create new Interface - self.cmd( - "iot pnp interface create -e {endpoint} -r {repo} --def {interface}", - checks=self.is_empty(), - ) - - # Checking the Interface list - self.cmd( - "iot pnp interface list -e {endpoint} -r {repo}", - checks=[ - self.greater_than("length([*])", 0), - self.exists("[?urnId==`{}`]".format(self.kwargs["interface_id"])), - ], - ) - - # Get Interface - interface = self.cmd( - "iot pnp interface show -e {endpoint} -r {repo} -i {interface_id}" - ).get_output_in_json() - assert json.dumps(interface) - assert interface["@id"] == self.kwargs["interface_id"] - assert interface["displayName"] == "MXChip1" - assert len(interface["contents"]) > 0 - - # Success: Update Interface - interface = str(read_file_content(self.kwargs["interface"])) - display_name = json.loads(interface)["displayName"] - interface_newContent = interface.replace( - display_name, "{}-Updated".format(display_name) - ) - interface_newContent = interface_newContent.replace("\n", "") - fo = open(self.kwargs["interface_updated"], "w+", encoding="utf-8") - fo.write(interface_newContent) - fo.close() - self.cmd( - "iot pnp interface update -e {endpoint} -r {repo} --def {interface_updated}", - checks=self.is_empty(), - ) - - # Publish Interface - # Error must be published from partner tenant - self.cmd( - "iot pnp interface publish -e {endpoint} -r {repo} -i {interface_id}", - expect_failure=True, - ) - - # Success: Delete Interface - self.cmd( - "iot pnp interface delete -e {endpoint} -r {repo} -i {interface_id}", - checks=self.is_empty(), - ) - - def test_model_life_cycle(self): - - # Error: missing repo-id or login - self.cmd( - "iot pnp capability-model create -e {endpoint} --def {model}", - expect_failure=True, - ) - - # Error: Invalid Capability-Model definition file - self.cmd( - "iot pnp capability-model create -e {endpoint} -r {repo} --def model", - expect_failure=True, - ) - - # Error: wrong path of Capability-Model definition - self.cmd( - "iot pnp capability-model create -e {endpoint} -r {repo} --def model.json", - expect_failure=True, - ) - - # Success: Create new Capability-Model - self.cmd( - "iot pnp capability-model create -e {endpoint} -r {repo} --def {model}", - checks=self.is_empty(), - ) - - # Checking the Capability-Model list - self.cmd( - "iot pnp capability-model list -e {endpoint} -r {repo}", - checks=[ - self.greater_than("length([*])", 0), - self.exists("[?urnId==`{}`]".format(self.kwargs["model_id"])), - ], - ) - - # Get Capability-Model - model = self.cmd( - "iot pnp capability-model show -e {endpoint} -r {repo} -m {model_id}" - ).get_output_in_json() - assert json.dumps(model) - assert model["@id"] == self.kwargs["model_id"] - assert len(model["implements"]) > 0 - - # Success: Update Capability-Model - model = str(read_file_content(self.kwargs["model"])) - display_name = json.loads(model)["displayName"] - model_newContent = model.replace( - display_name, "{}-Updated".format(display_name) - ) - model_newContent = model_newContent.replace("\n", "") - fo = open(self.kwargs["model_updated"], "w+", encoding="utf-8") - fo.write(model_newContent) - fo.close() - self.cmd( - "iot pnp capability-model update -e {endpoint} -r {repo} --def {model_updated}", - checks=self.is_empty(), - ) - - # Publish Capability-Model - # Error must be published from partner tenant - self.cmd( - "iot pnp capability-model publish -e {endpoint} -r {repo} -m {model_id}", - expect_failure=True, - ) - - # Success: Delete Capability-Model - self.cmd( - "iot pnp capability-model delete -e {endpoint} -r {repo} -m {model_id}", - checks=self.is_empty(), - ) diff --git a/azext_iot/tests/test_iot_pnp_unit.py b/azext_iot/tests/test_iot_pnp_unit.py deleted file mode 100644 index ee69fba0b..000000000 --- a/azext_iot/tests/test_iot_pnp_unit.py +++ /dev/null @@ -1,950 +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. -# -------------------------------------------------------------------------------------------- - -import pytest -import json -import os - -from uuid import uuid4 -from azext_iot.operations import pnp as subject -from azext_iot.common.utility import url_encode_str, read_file_content -from knack.util import CLIError -from .conftest import fixture_cmd, path_service_client, build_mock_response - -_repo_endpoint = "https://{}.{}".format(str(uuid4()), "com") -_repo_id = str(uuid4()).replace("-", "") -_repo_keyname = str(uuid4()).replace("-", "") -_repo_secret = "lMT+wSy8TIzDASRMlhxwpYxG3mWba45YqCFUo6Qngju5uZS9V4tM2yh5pn3zdB0FC3yRx91UnSWjdr/jLutPbg==" -generic_cs_template = ( - "HostName={};RepositoryId={};SharedAccessKeyName={};SharedAccessKey={}" -) -path_ghcs = "azext_iot.operations.pnp.get_iot_pnp_connection_string" -_pnp_create_interface_payload_file = "test_pnp_create_payload_interface.json" -_pnp_create_model_payload_file = "test_pnp_create_payload_model.json" -_pnp_show_interface_file = "test_pnp_interface_show.json" -_pnp_generic_interface_id = "urn:example:interfaces:MXChip:1" -_pnp_generic_model_id = "urn:example:capabilityModels:Mxchip:1" - - -@pytest.fixture() -def fixture_ghcs(mocker): - ghcs = mocker.patch(path_ghcs) - ghcs.return_value = mock_target - return ghcs - - -def generate_cs( - endpoint=_repo_endpoint, repository=_repo_id, policy=_repo_keyname, key=_repo_secret -): - return generic_cs_template.format(endpoint, repository, policy, key) - - -def change_dir(): - from inspect import getsourcefile - - os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0)))) - - -mock_target = {} -mock_target["cs"] = generate_cs() -mock_target["policy"] = _repo_keyname -mock_target["primarykey"] = _repo_secret -mock_target["repository_id"] = _repo_id -mock_target["entity"] = _repo_endpoint -mock_target["entity"] = mock_target["entity"].replace("https://", "") -mock_target["entity"] = mock_target["entity"].replace("http://", "") - - -def generate_pnp_interface_create_payload(content_from_file=False): - change_dir() - if content_from_file: - return (None, _pnp_create_interface_payload_file) - - return ( - str(read_file_content(_pnp_create_interface_payload_file)), - _pnp_create_interface_payload_file, - ) - - -class TestModelRepoInterfaceCreate(object): - @pytest.fixture(params=[201, 204, 412]) - def serviceclient(self, mocker, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.mark.parametrize( - "payload_scenario", - [ - (generate_pnp_interface_create_payload()), - (generate_pnp_interface_create_payload(content_from_file=True)), - ], - ) - def test_interface_create(self, fixture_cmd, serviceclient, payload_scenario): - payload = None - - # If file path provided - if not payload_scenario[0]: - payload = payload_scenario[1] - else: - payload = str(read_file_content(_pnp_create_interface_payload_file)) - - subject.iot_pnp_interface_create( - fixture_cmd, interface_definition=payload, login=mock_target["cs"] - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - data = args[0][0].data - headers = args[0][0].headers - - assert method == "PUT" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True) - ) - in url - ) - assert "repositoryId={}".format(_repo_id) in url - assert json.dumps(data) - assert headers.get("Authorization") - - @pytest.mark.parametrize( - "payload_scenario", [(generate_pnp_interface_create_payload())] - ) - def test_interface_create_error( - self, fixture_cmd, serviceclient_generic_error, payload_scenario - ): - with pytest.raises(CLIError): - subject.iot_pnp_interface_create( - fixture_cmd, - login=mock_target["cs"], - interface_definition=payload_scenario[0], - ) - - @pytest.mark.parametrize( - "payload_scenario, exp", [(generate_pnp_interface_create_payload(), CLIError)] - ) - def test_interface_create_invalid_args(self, serviceclient, payload_scenario, exp): - with pytest.raises(exp): - subject.iot_pnp_interface_create( - fixture_cmd, interface_definition=payload_scenario - ) - - def test_interface_create_invalid_payload(self, serviceclient): - - payload = str(read_file_content(_pnp_create_interface_payload_file)) - payload = json.loads(payload) - del payload["@id"] - payload = json.dumps(payload) - with pytest.raises(CLIError): - subject.iot_pnp_interface_create( - fixture_cmd, login=mock_target["cs"], interface_definition=payload - ) - - -class TestModelRepoInterfaceUpdate(object): - @pytest.fixture(params=[(200, 201), (200, 204), (200, 412)]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - payload = { - "continuationToken": "null", - "results": [ - { - "comment": "", - "createdOn": "2019-07-09T07:46:06.044161+00:00", - "description": "", - "displayName": "MXChip 1", - "etag": '"41006e67-0000-0800-0000-5d2501b80000"', - "modelName": "example:interfaces:MXChip", - "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", - "publisherName": "microsoft.com", - "type": "Interface", - "updatedOn": "2019-07-09T21:06:00.072063+00:00", - "urnId": "urn:example:interfaces:MXChip:1", - "version": 1, - } - ], - } - test_side_effect = [ - build_mock_response(mocker, request.param[0], payload=payload), - build_mock_response(mocker, request.param[1], {}), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize( - "payload_scenario", - [ - generate_pnp_interface_create_payload(), - generate_pnp_interface_create_payload(content_from_file=True), - ], - ) - def test_interface_update(self, fixture_cmd, serviceclient, payload_scenario): - payload = None - - # If file path provided - if not payload_scenario[0]: - payload = payload_scenario[1] - else: - payload = str(read_file_content(_pnp_create_interface_payload_file)) - - subject.iot_pnp_interface_update( - fixture_cmd, - interface_definition=payload, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - data = args[0][0].data - headers = args[0][0].headers - - assert method == "PUT" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True) - ) - in url - ) - assert "repositoryId={}".format(_repo_id) in url - assert json.dumps(data) - assert headers.get("Authorization") - - @pytest.mark.parametrize( - "payload_scenario", - [ - (generate_pnp_interface_create_payload()), - (generate_pnp_interface_create_payload(content_from_file=True)), - ], - ) - def test_model_update_error( - self, fixture_cmd, serviceclient_generic_error, payload_scenario - ): - if not payload_scenario[0]: - payload = payload_scenario[1] - else: - payload = str(read_file_content(_pnp_create_interface_payload_file)) - with pytest.raises(CLIError): - subject.iot_pnp_interface_update( - fixture_cmd, - interface_definition=payload, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - @pytest.mark.parametrize( - "payload_scenario, exp", [(generate_pnp_interface_create_payload(), CLIError)] - ) - def test_interface_update_invalid_args(self, serviceclient, payload_scenario, exp): - with pytest.raises(exp): - subject.iot_pnp_interface_update( - fixture_cmd, interface_definition=payload_scenario - ) - - def test_interface_update_invalid_payload(self, serviceclient): - - payload = str(read_file_content(_pnp_create_interface_payload_file)) - payload = json.loads(payload) - payload["@id"] = "fake_invalid_id" - payload = json.dumps(payload) - with pytest.raises(CLIError): - subject.iot_pnp_interface_update( - fixture_cmd, - interface_definition=payload, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - -class TestModelRepoInterfacePublish(object): - @pytest.fixture(params=[(200, 200, 201), (200, 200, 204), (200, 200, 412)]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - payload_list = { - "continuationToken": "null", - "results": [ - { - "comment": "", - "createdOn": "2019-07-09T07:46:06.044161+00:00", - "description": "", - "displayName": "MXChip 1", - "etag": '"41006e67-0000-0800-0000-5d2501b80000"', - "modelName": "example:interfaces:MXChip", - "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", - "publisherName": "microsoft.com", - "type": "Interface", - "updatedOn": "2019-07-09T21:06:00.072063+00:00", - "urnId": "urn:example:interfaces:MXChip:1", - "version": 1, - } - ], - } - payload_show = { - "@id": "urn:example:interfaces:MXChip:1", - "@type": "Interface", - "displayName": "MXChip 1", - "contents": [ - { - "@type": "Property", - "displayName": "Die Number", - "name": "dieNumber", - "schema": "double", - } - ], - "@context": "http://azureiot.com/v1/contexts/Interface.json", - } - test_side_effect = [ - build_mock_response(mocker, request.param[0], payload=payload_list), - build_mock_response(mocker, request.param[1], payload=payload_show), - build_mock_response(mocker, request.param[2], {}), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)]) - def test_interface_publish(self, fixture_cmd, serviceclient, target_interface): - subject.iot_pnp_interface_publish( - fixture_cmd, interface=target_interface, login=mock_target["cs"] - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - data = args[0][0].data - headers = args[0][0].headers - - assert method == "PUT" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True) - ) - in url - ) - assert json.loads(data)["@id"] == _pnp_generic_interface_id - assert headers.get("Authorization") - - @pytest.mark.parametrize("target_interface", [("acv.17")]) - def test_interface_publish_error( - self, fixture_cmd, serviceclient_generic_error, target_interface - ): - with pytest.raises(CLIError): - subject.iot_pnp_interface_publish( - fixture_cmd, interface=target_interface, login=mock_target["cs"] - ) - - -class TestModelRepoInterfaceShow(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - payload = { - "@id": "urn:example:interfaces:MXChip:1", - "@type": "Interface", - "displayName": "MXChip 1", - "contents": [ - { - "@type": "Property", - "displayName": "Die Number", - "name": "dieNumber", - "schema": "double", - } - ], - "@context": "http://azureiot.com/v1/contexts/Interface.json", - } - service_client.return_value = build_mock_response( - mocker, request.param, payload - ) - return service_client - - @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)]) - def test_interface_show(self, fixture_cmd, serviceclient, target_interface): - result = subject.iot_pnp_interface_show( - fixture_cmd, - interface=target_interface, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - headers = args[0][0].headers - - assert method == "GET" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True) - ) - in url - ) - assert "repositoryId={}".format(_repo_id) in url - assert json.dumps(result) - assert headers.get("Authorization") - - @pytest.fixture(params=[200]) - def serviceclientemptyresult(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)]) - def test_interface_show_error( - self, fixture_cmd, serviceclientemptyresult, target_interface - ): - with pytest.raises(CLIError): - subject.iot_pnp_interface_show( - fixture_cmd, - interface=target_interface, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - -class TestModelRepoInterfaceList(object): - @pytest.fixture(params=[200]) - def service_client(self, mocker, fixture_ghcs, request): - serviceclient = mocker.patch(path_service_client) - payload = { - "continuationToken": "null", - "results": [ - { - "comment": "", - "createdOn": "2019-07-09T07:46:06.044161+00:00", - "description": "", - "displayName": "MXChip 1", - "etag": '"41006e67-0000-0800-0000-5d2501b80000"', - "modelName": "example:interfaces:MXChip", - "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", - "publisherName": "microsoft.com", - "type": "Interface", - "updatedOn": "2019-07-09T21:06:00.072063+00:00", - "urnId": "urn:example:interfaces:MXChip:1", - "version": 1, - } - ], - } - serviceclient.return_value = build_mock_response(mocker, request.param, payload) - return serviceclient - - def test_interface_list(self, fixture_cmd, service_client): - result = subject.iot_pnp_interface_list( - fixture_cmd, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - args = service_client.call_args - url = args[0][0].url - method = args[0][0].method - headers = args[0][0].headers - - assert method == "POST" - assert "{}/models/search?".format(_repo_endpoint) in url - assert "repositoryId={}".format(_repo_id) in url - assert len(result) == 1 - assert headers.get("Authorization") - - def test_interface_list_error(self, fixture_cmd, serviceclient_generic_error): - with pytest.raises(CLIError): - subject.iot_pnp_interface_list( - fixture_cmd, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - -class TestModelRepoInterfaceDelete(object): - @pytest.fixture(params=[204]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)]) - def test_interface_delete(self, fixture_cmd, serviceclient, target_interface): - subject.iot_pnp_interface_delete( - fixture_cmd, - interface=target_interface, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - headers = args[0][0].headers - - assert method == "DELETE" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True) - ) - in url - ) - assert "repositoryId={}".format(_repo_id) in url - assert headers.get("Authorization") - - @pytest.mark.parametrize("target_interface", [("acv.17")]) - def test_model_delete_error( - self, fixture_cmd, serviceclient_generic_error, target_interface - ): - with pytest.raises(CLIError): - subject.iot_pnp_interface_delete( - fixture_cmd, - interface=target_interface, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - -def generate_pnp_model_create_payload(content_from_file=False): - change_dir() - if content_from_file: - return (None, _pnp_create_model_payload_file) - - return ( - str(read_file_content(_pnp_create_model_payload_file)), - _pnp_create_model_payload_file, - ) - - -class TestModelRepoModelCreate(object): - @pytest.fixture(params=[201, 204, 412]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.mark.parametrize( - "payload_scenario", - [ - (generate_pnp_model_create_payload()), - (generate_pnp_model_create_payload(content_from_file=True)), - ], - ) - def test_model_create(self, fixture_cmd, serviceclient, payload_scenario): - - payload = None - - # If file path provided - if not payload_scenario[0]: - payload = payload_scenario[1] - else: - payload = str(read_file_content(_pnp_create_model_payload_file)) - - subject.iot_pnp_model_create( - fixture_cmd, - model_definition=payload, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - data = args[0][0].data - headers = args[0][0].headers - - assert method == "PUT" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True) - ) - in url - ) - assert "repositoryId={}".format(_repo_id) in url - assert json.dumps(data) - assert headers.get("Authorization") - - @pytest.mark.parametrize( - "payload_scenario", - [ - (generate_pnp_model_create_payload()), - (generate_pnp_model_create_payload(content_from_file=True)), - ], - ) - def test_model_create_error( - self, fixture_cmd, serviceclient_generic_error, payload_scenario - ): - if not payload_scenario[0]: - payload = payload_scenario[1] - else: - payload = str(read_file_content(_pnp_create_model_payload_file)) - with pytest.raises(CLIError): - subject.iot_pnp_model_create( - fixture_cmd, - model_definition=payload, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - @pytest.mark.parametrize( - "payload_scenario, exp", [(generate_pnp_model_create_payload(), CLIError)] - ) - def test_model_create_invalid_args(self, serviceclient, payload_scenario, exp): - with pytest.raises(exp): - subject.iot_pnp_model_create(fixture_cmd, model_definition=payload_scenario) - - def test_model_create_invalid_payload(self, serviceclient): - - payload = str(read_file_content(_pnp_create_model_payload_file)) - payload = json.loads(payload) - del payload["@id"] - payload = json.dumps(payload) - with pytest.raises(CLIError): - subject.iot_pnp_model_create( - fixture_cmd, login=mock_target["cs"], model_definition=payload - ) - - -class TestModelRepoModelUpdate(object): - @pytest.fixture(params=[(200, 201), (200, 204), (200, 412)]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - payload = { - "continuationToken": "null", - "results": [ - { - "comment": "", - "createdOn": "2019-07-09T07:46:06.044161+00:00", - "description": "", - "displayName": "Mxchip 1", - "etag": '"41006e67-0000-0800-0000-5d2501b80000"', - "modelName": "example:capabilityModels:Mxchip", - "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", - "publisherName": "microsoft.com", - "type": "CapabilityModel", - "updatedOn": "2019-07-09T21:06:00.072063+00:00", - "urnId": "urn:example:capabilityModels:Mxchip:1", - "version": 1, - } - ], - } - test_side_effect = [ - build_mock_response(mocker, request.param[0], payload=payload), - build_mock_response(mocker, request.param[1], {}), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize( - "payload_scenario", - [ - generate_pnp_model_create_payload(), - generate_pnp_model_create_payload(content_from_file=True), - ], - ) - def test_model_update(self, fixture_cmd, serviceclient, payload_scenario): - - payload = None - - # If file path provided - if not payload_scenario[0]: - payload = payload_scenario[1] - else: - payload = str(read_file_content(_pnp_create_model_payload_file)) - - subject.iot_pnp_model_update( - fixture_cmd, - model_definition=payload, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - data = args[0][0].data - headers = args[0][0].headers - - assert method == "PUT" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True) - ) - in url - ) - assert "repositoryId={}".format(_repo_id) in url - assert json.dumps(data) - assert headers.get("Authorization") - - @pytest.mark.parametrize( - "payload_scenario", - [ - (generate_pnp_model_create_payload()), - (generate_pnp_model_create_payload(content_from_file=True)), - ], - ) - def test_model_update_error( - self, fixture_cmd, serviceclient_generic_error, payload_scenario - ): - if not payload_scenario[0]: - payload = payload_scenario[1] - else: - payload = str(read_file_content(_pnp_create_model_payload_file)) - with pytest.raises(CLIError): - subject.iot_pnp_model_update( - fixture_cmd, - model_definition=payload, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - @pytest.mark.parametrize( - "payload_scenario, exp", [(generate_pnp_model_create_payload(), CLIError)] - ) - def test_model_update_invalid_args(self, serviceclient, payload_scenario, exp): - with pytest.raises(exp): - subject.iot_pnp_model_update(fixture_cmd, model_definition=payload_scenario) - - def test_model_update_invalid_payload(self, serviceclient): - - payload = str(read_file_content(_pnp_create_model_payload_file)) - payload = json.loads(payload) - payload["@id"] = "fake_invalid_id" - payload = json.dumps(payload) - with pytest.raises(CLIError): - subject.iot_pnp_model_update( - fixture_cmd, - model_definition=payload, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - -class TestModelRepoModelPublish(object): - @pytest.fixture(params=[(200, 200, 201), (200, 200, 204), (200, 200, 412)]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - payload_list = { - "continuationToken": "null", - "results": [ - { - "comment": "", - "createdOn": "2019-07-09T07:46:06.044161+00:00", - "description": "", - "displayName": "Mxchip 1", - "etag": '"41006e67-0000-0800-0000-5d2501b80000"', - "modelName": "example:capabilityModels:Mxchip", - "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", - "publisherName": "microsoft.com", - "type": "CapabilityModel", - "updatedOn": "2019-07-09T21:06:00.072063+00:00", - "urnId": "urn:example:capabilityModels:Mxchip:1", - "version": 1, - } - ], - } - payload_show = { - "@id": "urn:example:capabilityModels:Mxchip:1", - "@type": "CapabilityModel", - "displayName": "Mxchip 1", - "implements": [ - {"schema": "urn:example:interfaces:MXChip:1", "name": "MXChip1"} - ], - "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json", - } - test_side_effect = [ - build_mock_response(mocker, request.param[0], payload=payload_list), - build_mock_response(mocker, request.param[1], payload=payload_show), - build_mock_response(mocker, request.param[2], {}), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) - def test_model_publish(self, fixture_cmd, serviceclient, target_model): - subject.iot_pnp_model_publish( - fixture_cmd, model=target_model, login=mock_target["cs"] - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - data = args[0][0].data - headers = args[0][0].headers - - assert method == "PUT" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True) - ) - in url - ) - assert json.loads(data)["@id"] == _pnp_generic_model_id - assert headers.get("Authorization") - - @pytest.mark.parametrize("target_model", [("acv.17")]) - def test_model_publish_error( - self, fixture_cmd, serviceclient_generic_error, target_model - ): - with pytest.raises(CLIError): - subject.iot_pnp_model_publish( - fixture_cmd, model=target_model, login=mock_target["cs"] - ) - - -class TestModelRepoModelShow(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - payload = { - "@id": "urn:example:capabilityModels:Mxchip:1", - "@type": "CapabilityModel", - "displayName": "Mxchip 1", - "implements": [ - {"schema": "urn:example:interfaces:MXChip:1", "name": "MXChip1"} - ], - "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json", - } - service_client.return_value = build_mock_response( - mocker, request.param, payload=payload - ) - return service_client - - @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) - def test_model_show(self, fixture_cmd, serviceclient, target_model): - result = subject.iot_pnp_model_show( - fixture_cmd, - model=target_model, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - headers = args[0][0].headers - - assert method == "GET" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True) - ) - in url - ) - assert "repositoryId={}".format(_repo_id) in url - assert json.dumps(result) - assert headers.get("Authorization") - - @pytest.mark.parametrize("target_model", [("acv:17")]) - def test_model_show_error( - self, fixture_cmd, serviceclient_generic_error, target_model - ): - with pytest.raises(CLIError): - subject.iot_pnp_model_show( - fixture_cmd, - model=target_model, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - @pytest.fixture(params=[200]) - def serviceclientemptyresult(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) - def test_model_show_no_result( - self, fixture_cmd, serviceclientemptyresult, target_model - ): - with pytest.raises(CLIError): - subject.iot_pnp_model_show( - fixture_cmd, - model=target_model, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - -class TestModelRepoModelList(object): - @pytest.fixture(params=[200]) - def service_client(self, mocker, fixture_ghcs, request): - serviceclient = mocker.patch(path_service_client) - payload = { - "continuationToken": "null", - "results": [ - { - "comment": "", - "createdOn": "2019-07-09T07:46:06.044161+00:00", - "description": "", - "displayName": "Mxchip 1", - "etag": '"41006e67-0000-0800-0000-5d2501b80000"', - "modelName": "example:capabilityModels:Mxchip", - "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", - "publisherName": "microsoft.com", - "type": "CapabilityModel", - "updatedOn": "2019-07-09T21:06:00.072063+00:00", - "urnId": "urn:example:capabilityModels:Mxchip:1", - "version": 1, - } - ], - } - serviceclient.return_value = build_mock_response(mocker, request.param, payload) - return serviceclient - - def test_model_list(self, fixture_cmd, service_client): - result = subject.iot_pnp_model_list( - fixture_cmd, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - args = service_client.call_args - url = args[0][0].url - method = args[0][0].method - headers = args[0][0].headers - - assert method == "POST" - assert "{}/models/search?".format(_repo_endpoint) in url - assert "repositoryId={}".format(_repo_id) in url - assert len(result) == 1 - assert headers.get("Authorization") - - def test_model_list_error(self, fixture_cmd, serviceclient_generic_error): - with pytest.raises(CLIError): - subject.iot_pnp_model_list( - fixture_cmd, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - - -class TestModelRepoModelDelete(object): - @pytest.fixture(params=[204]) - def serviceclient(self, mocker, fixture_ghcs, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) - def test_model_delete(self, fixture_cmd, serviceclient, target_model): - subject.iot_pnp_model_delete( - fixture_cmd, - model=target_model, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - headers = args[0][0].headers - - assert method == "DELETE" - assert ( - "{}/models/{}?".format( - _repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True) - ) - in url - ) - assert "repositoryId={}".format(_repo_id) in url - assert headers.get("Authorization") - - @pytest.mark.parametrize("target_model", [("acv.17")]) - def test_model_delete_error( - self, fixture_cmd, serviceclient_generic_error, target_model - ): - with pytest.raises(CLIError): - subject.iot_pnp_model_delete( - fixture_cmd, - model=target_model, - repo_endpoint=mock_target["entity"], - repo_id=mock_target["repository_id"], - ) diff --git a/azext_iot/tests/test_monitor_parsers_unit.py b/azext_iot/tests/test_monitor_parsers_unit.py index 0dd3bb31f..36cb9a6e6 100644 --- a/azext_iot/tests/test_monitor_parsers_unit.py +++ b/azext_iot/tests/test_monitor_parsers_unit.py @@ -44,6 +44,16 @@ def _validate_issues( assert expected_details in actual_messages +@pytest.fixture( + params=[ + common_parser.INTERFACE_NAME_IDENTIFIER_V1, + common_parser.INTERFACE_NAME_IDENTIFIER_V2, + ] +) +def interface_identifier_bytes(request): + return request.param + + class TestCommonParser: device_id = "some-device-id" payload = {"String": "someValue"} @@ -55,13 +65,15 @@ class TestCommonParser: bad_content_type = "bad-content-type" @pytest.mark.parametrize( - "device_id, encoding, content_type, interface_name, module_id, payload, properties, app_properties", + "device_id, encoding, content_type, interface_name, component_name, " + "module_id, payload, properties, app_properties", [ ( "device-id", "utf-8", "application/json", "interface_name", + "component_name", "module-id", {"payloadKey": "payloadValue"}, {"propertiesKey": "propertiesValue"}, @@ -72,6 +84,18 @@ class TestCommonParser: "utf-8", "application/json", "interface_name", + "component_name", + "", + {"payloadKey": "payloadValue"}, + {"propertiesKey": "propertiesValue"}, + {"appPropsKey": "appPropsValue"}, + ), + ( + "device-id", + "utf-8", + "application/json", + "interface_name", + "", "", {"payloadKey": "payloadValue"}, {"propertiesKey": "propertiesValue"}, @@ -83,6 +107,7 @@ class TestCommonParser: "application/json", "", "", + "", {"payloadKey": "payloadValue"}, {"propertiesKey": "propertiesValue"}, {"appPropsKey": "appPropsValue"}, @@ -93,6 +118,7 @@ class TestCommonParser: "application/json", "", "", + "", {}, {"propertiesKey": "propertiesValue"}, {"appPropsKey": "appPropsValue"}, @@ -103,11 +129,12 @@ class TestCommonParser: "application/json", "", "", + "", {}, {}, {"appPropsKey": "appPropsValue"}, ), - ("device-id", "utf-8", "application/json", "", "", {}, {}, {},), + ("device-id", "utf-8", "application/json", "", "", "", {}, {}, {}), ], ) def test_parse_message_should_succeed( @@ -116,10 +143,12 @@ def test_parse_message_should_succeed( encoding, content_type, interface_name, + component_name, payload, properties, app_properties, module_id, + interface_identifier_bytes, ): # setup properties = MessageProperties( @@ -130,8 +159,9 @@ def test_parse_message_should_succeed( properties=properties, annotations={ common_parser.DEVICE_ID_IDENTIFIER: device_id.encode(), - common_parser.INTERFACE_NAME_IDENTIFIER: interface_name.encode(), + interface_identifier_bytes: interface_name.encode(), common_parser.MODULE_ID_IDENTIFIER: module_id.encode(), + common_parser.COMPONENT_NAME_IDENTIFIER: component_name.encode(), }, application_properties=_encode_app_props(app_properties), ) @@ -156,6 +186,23 @@ def test_parse_message_should_succeed( assert properties["system"]["content_type"] == content_type assert properties["application"] == app_properties + assert parsed_msg["event"]["interface"] == interface_name + assert parsed_msg["event"]["component"] == component_name + + if interface_name: + interface_identifier = str(interface_identifier_bytes, "utf8") + assert ( + parsed_msg["event"]["annotations"][interface_identifier] + == interface_name + ) + + if component_name: + component_identifier = str(common_parser.COMPONENT_NAME_IDENTIFIER, "utf8") + assert ( + parsed_msg["event"]["annotations"][component_identifier] + == component_name + ) + assert len(parser.issues_handler.get_all_issues()) == 0 def test_parse_message_bad_content_type_should_warn(self): diff --git a/azext_iot/tests/test_pnp_create_payload_model.json b/azext_iot/tests/test_pnp_create_payload_model.json deleted file mode 100644 index 1ba329d4d..000000000 --- a/azext_iot/tests/test_pnp_create_payload_model.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "@id": "urn:example:capabilityModels:Mxchip:1", - "@type": "CapabilityModel", - "displayName": "Mxchip 1", - "implements": [{ - "schema": "urn:example:interfaces:MXChip:1", - "name": "MXChip1" - }], - "@context": "http://azureiot.com/v1/contexts/IoTModel.json" -} \ No newline at end of file diff --git a/azext_iot/tests/test_pnp_interface_list.json b/azext_iot/tests/test_pnp_interface_list.json deleted file mode 100644 index a0da071cd..000000000 --- a/azext_iot/tests/test_pnp_interface_list.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "continuationToken": null, - "results": [{ - "comment": "Requires temperature and humidity sensors.", - "createdOn": "2019-07-03T22:42:21.929126+00:00", - "description": "Provides functionality to report temperature, humidity. Provides telemetry, commands and read-write properties", - "displayName": "Environmental Sensor", - "etag": "\"110043fc-0000-0800-0000-5d27a3960000\"", - "modelName": "contoso:com:EnvironmentalSensor", - "publisherId": "72f988bf-86f1-41af-91ab-2d7cd011db47", - "publisherName": "microsoft.com", - "type": "Interface", - "updatedOn": "2019-07-03T22:42:21.929126+00:00", - "urnId": "urn:contoso:com:EnvironmentalSensor:1", - "version": 1 - }] -} \ No newline at end of file diff --git a/pytest.ini.example b/pytest.ini.example index d2d09a564..548e1ca1f 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -19,9 +19,6 @@ env = azext_iot_testdps= azext_iot_teststorageuri= azext_iot_testidentity= - azext_pnp_endpoint= - azext_pnp_repository= - azext_pnp_cs= azext_iot_central_app_id= azext_dt_rbac_assignee_owner= azext_dt_rbac_assignee_reader= From 3422aaa8277919c776f40b19bb5b49b4fb91c2cc Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 14 Jul 2020 19:02:06 -0700 Subject: [PATCH 064/179] Increment IoT ext version --- azext_iot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azext_iot/constants.py b/azext_iot/constants.py index f666c5bdd..949e20c36 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.6" +VERSION = "0.9.7" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" From 2c9f68ac7acec051666d891744deea6236f8770e Mon Sep 17 00:00:00 2001 From: Ryan K Date: Wed, 15 Jul 2020 14:06:11 -0700 Subject: [PATCH 065/179] Removed --created-by parameter from search options (#218) --- azext_iot/pnp/_help.py | 4 ++-- azext_iot/pnp/commands_api.py | 2 -- azext_iot/pnp/params.py | 7 +------ azext_iot/tests/pnp/test_iot_pnp_unit.py | 2 -- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/azext_iot/pnp/_help.py b/azext_iot/pnp/_help.py index 2b802af8a..2ef82f2d2 100644 --- a/azext_iot/pnp/_help.py +++ b/azext_iot/pnp/_help.py @@ -125,9 +125,9 @@ def load_pnp_help(): text: > az iot pnp model list - - name: Search for all 'Listed' models created by a specific user or spn + - name: Search for all 'Listed' models published by a specific tenant text: > - az iot pnp model list --state Listed --created-by {user_or_spn_id} + az iot pnp model list --state Listed --publisher-id {tenant_id} - name: Search for shared interfaces with name or description matching `{keyword}` text: > diff --git a/azext_iot/pnp/commands_api.py b/azext_iot/pnp/commands_api.py index 271577424..fe9c00870 100644 --- a/azext_iot/pnp/commands_api.py +++ b/azext_iot/pnp/commands_api.py @@ -24,7 +24,6 @@ def iot_pnp_model_list( model_type=None, model_state=None, publisher_id=None, - created_by=None, shared=False, top=None, pnp_dns_suffix=None, @@ -35,7 +34,6 @@ def iot_pnp_model_list( model_type=model_type, model_state=model_state, publisher_id=publisher_id, - created_by=created_by, ) return ap.search_models(search_options, shared, _process_top(top)) diff --git a/azext_iot/pnp/params.py b/azext_iot/pnp/params.py index fc204fc8b..aa147e36e 100644 --- a/azext_iot/pnp/params.py +++ b/azext_iot/pnp/params.py @@ -85,11 +85,6 @@ def load_pnp_arguments(self, _): options_list=["--keyword", "-q"], help="Restrict model list to those matching a provided keyword", ) - context.argument( - "created_by", - options_list=["--created-by"], - help="Restrict model list to models created by a specific user or service principal", - ) context.argument( "model_state", arg_type=get_enum_type(ModelState), @@ -105,7 +100,7 @@ def load_pnp_arguments(self, _): context.argument( "publisher_id", options_list=["--publisher-id", "--pub"], - help="Restrict model list to models published by a specific user or service principal", + help="Restrict model list to models published by a specific tenant", ) context.argument( "shared", diff --git a/azext_iot/tests/pnp/test_iot_pnp_unit.py b/azext_iot/tests/pnp/test_iot_pnp_unit.py index d82872063..67c034ae1 100644 --- a/azext_iot/tests/pnp/test_iot_pnp_unit.py +++ b/azext_iot/tests/pnp/test_iot_pnp_unit.py @@ -285,7 +285,6 @@ def serviceclient_generic_error(self, mocker, fixture_sas, request): "modelType": "type", "modelState": "state", "publisherId": "publisher", - "createdBy": "creator", }, False, ), @@ -300,7 +299,6 @@ def test_model_list(self, fixture_cmd, service_client, search_content, shared): model_type=search_content.get("modelType"), model_state=search_content.get("modelState"), publisher_id=search_content.get("publisherId"), - created_by=search_content.get("createdBy"), shared=shared, ) args = service_client.call_args From f14c7f9759bc792359adae97e6ee33c712b211b6 Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Thu, 16 Jul 2020 14:35:53 -0700 Subject: [PATCH 066/179] Enabling running IT's as part of release pipeline (#215) * enable ITs * minor update to pulling env variables * Update reading env * Added the rest of the env variables * Minor script issue * Try to login to azure * displayName issue * Plumbed 'token' through all relevant Central code paths * Update test script, use -y flag * Fixed UT's * Minor update to params.py * Make token optional everywhere * Fix sentinel values * added retry logic for deleting device template in IT's * Add service principal to central command group * remove references to token since we can now add service principal * Remove device list, device template list/map * Changed add user pattern * Remove references to pnp in sentinel * Apply teeny nit for placeholder variables in help text. --- .azure-devops/create-release.yml | 1 + .azure-devops/templates/run-all-tests.yml | 8 ++ .azure-devops/templates/run-tests.yml | 4 +- .../templates/set-testenv-sentinel.yml | 30 ++++-- azext_iot/central/_help.py | 73 +++++++-------- azext_iot/central/command_map.py | 13 ++- azext_iot/central/commands_device_twin.py | 6 +- azext_iot/central/commands_monitor.py | 13 ++- azext_iot/central/commands_user.py | 41 ++++++++ azext_iot/central/models/enum.py | 20 ++++ azext_iot/central/params.py | 44 +++++++++ azext_iot/central/providers/__init__.py | 2 + .../central/providers/devicetwin_provider.py | 7 +- .../central/providers/monitor_provider.py | 13 ++- azext_iot/central/providers/user_provider.py | 78 ++++++++++++++++ azext_iot/central/services/__init__.py | 4 +- azext_iot/central/services/user.py | 93 +++++++++++++++++++ azext_iot/common/_azure.py | 14 +-- .../builders/central_target_builder.py | 14 ++- azext_iot/monitor/property.py | 22 +++-- azext_iot/tests/test_iot_central_int.py | 48 ++++------ azext_iot/tests/test_iot_central_unit.py | 32 +++++-- 22 files changed, 465 insertions(+), 115 deletions(-) create mode 100644 .azure-devops/templates/run-all-tests.yml create mode 100644 azext_iot/central/commands_user.py create mode 100644 azext_iot/central/providers/user_provider.py create mode 100644 azext_iot/central/services/user.py diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index 0ae28e069..d7aadd474 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -41,6 +41,7 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.7.x' + runUnitTestsOnly: 'false' - script: 'pip install .' displayName: 'Install the whl locally' diff --git a/.azure-devops/templates/run-all-tests.yml b/.azure-devops/templates/run-all-tests.yml new file mode 100644 index 000000000..e31b8b16d --- /dev/null +++ b/.azure-devops/templates/run-all-tests.yml @@ -0,0 +1,8 @@ +steps: +- task: AzureCLI@2 + displayName: 'Run all tests' + inputs: + azureSubscription: AzIoTCLIService + scriptType: bash + scriptLocation: inlineScript + inlineScript: pytest -vv -rf azext_iot/tests/ --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-full-results.xml diff --git a/.azure-devops/templates/run-tests.yml b/.azure-devops/templates/run-tests.yml index d9e164234..4fa07db98 100644 --- a/.azure-devops/templates/run-tests.yml +++ b/.azure-devops/templates/run-tests.yml @@ -24,8 +24,8 @@ steps: - template: set-pythonpath.yml - ${{ if eq(parameters.runUnitTestsOnly, 'false') }}: - - script: pytest --junitxml "TEST-results.xml" - displayName: 'Execute all Tests' + - template: set-testenv-sentinel.yml + - template: run-all-tests.yml - ${{ if eq(parameters.runUnitTestsOnly, 'true') }}: - template: set-testenv-sentinel.yml diff --git a/.azure-devops/templates/set-testenv-sentinel.yml b/.azure-devops/templates/set-testenv-sentinel.yml index 88de2ae57..ef14978cb 100644 --- a/.azure-devops/templates/set-testenv-sentinel.yml +++ b/.azure-devops/templates/set-testenv-sentinel.yml @@ -7,18 +7,32 @@ steps: script: | # This task is in place to get around DevOps pipelines env var naming rules which would require # application code changes. + import os sentinel_value = "sentinel" - envvars = [ - "azext_iot_testhub", - "azext_iot_testhub_cs", - "azext_iot_testrg", - "azext_iot_testdps", - "azext_iot_central_app_id", - ] + envvars = { + "AZURE_TEST_RUN_LIVE":True, + "azext_iot_testrg": os.environ.get("AZEXT_IOT_TESTRG", sentinel_value), + "azext_iot_testhub": os.environ.get("AZEXT_IOT_TESTHUB", sentinel_value), + "azext_iot_testhub_cs": os.environ.get("AZEXT_IOT_TESTHUB_CS", 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_testidentity": os.environ.get("AZEXT_IOT_TESTIDENTITY", sentinel_value), + "azext_iot_central_app_id": os.environ.get("AZEXT_IOT_CENTRAL_APP_ID", sentinel_value), + } f = open("./pytest.ini", "w+") f.write("[pytest]\n") f.write("junit_family = xunit1\n") f.write("env = \n") - envvars_sentinel = [" {}={}\n".format(envvar, sentinel_value) for envvar in envvars] + envvars_sentinel = [" {}={}\n".format(key, val) for key, val in envvars.items()] f.writelines(envvars_sentinel) + print(envvars_sentinel) f.close() + + env: + AZEXT_IOT_TESTRG: $(azext_iot_testrg) + AZEXT_IOT_TESTHUB: $(azext_iot_testhub) + AZEXT_IOT_TESTHUB_CS: $(azext_iot_testhub_cs) + AZEXT_IOT_TESTDPS: $(azext_iot_testdps) + AZEXT_IOT_TESTSTORAGEURI: $(azext_iot_teststorageuri) + AZEXT_IOT_TESTIDENTITY: $(azext_iot_testidentity) + AZEXT_IOT_CENTRAL_APP_ID: $(azext_iot_central_app_id) \ No newline at end of file diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index ff29b4db1..8244a39bc 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -28,6 +28,7 @@ def load_central_help(): """ _load_central_devices_help() + _load_central_users_help() _load_central_device_templates_help() _load_central_device_twin_help() _load_central_monitors_help() @@ -80,19 +81,6 @@ def _load_central_devices_help(): --device-id {deviceid} """ - helps[ - "iot central app device list" - ] = """ - type: command - short-summary: List all devices in IoT Central - - examples: - - name: List all devices in IoT Central - text: > - az iot central app device list - --app-id {appid} - """ - helps[ "iot central app device delete" ] = """ @@ -196,6 +184,39 @@ def _load_central_devices_help(): """ +def _load_central_users_help(): + helps[ + "iot central app user" + ] = """ + type: group + short-summary: Manage and configure IoT Central users + """ + + helps[ + "iot central app user create" + ] = """ + type: command + short-summary: Add a user to the app + examples: + - name: Add a user with email to the app + text: > + az iot central app user create + --id {userId} + --app-id {appId} + --email {emailAddress} + --role admin + + - name: Add a service-principal to the app + text: > + az iot central app user create + --id {userId} + --app-id {appId} + --tenant-id {tenantId} + --object-id {objectId} + --role operator + """ + + def _load_central_device_templates_help(): helps[ "iot central app device-template" @@ -240,32 +261,6 @@ def _load_central_device_templates_help(): --device-template-id {devicetemplateid} """ - helps[ - "iot central app device-template list" - ] = """ - type: command - short-summary: List all device templates in IoT Central - - examples: - - name: List all device templates - text: > - az iot central app device-template list - --app-id {appid} - """ - - helps[ - "iot central app device-template map" - ] = """ - type: command - short-summary: Returns a mapping of device template name to device template id - - examples: - - name: Get device template name to id mapping - text: > - az iot central app device-template map - --app-id {appid} - """ - helps[ "iot central app device-template delete" ] = """ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index af4e7ff45..81980ce06 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -25,6 +25,8 @@ operations_tmpl="azext_iot.central.commands_monitor#{}" ) +central_user_ops = CliCommandType(operations_tmpl="azext_iot.central.commands_user#{}") + # Dev note - think of this as the "router" and all self.command_group as the controllers def load_central_commands(self, _): @@ -39,10 +41,15 @@ def load_central_commands(self, _): cmd_group.command("monitor-properties", "monitor_properties", is_preview=True) cmd_group.command("validate-properties", "validate_properties", is_preview=True) + with self.command_group( + "iot central app user", command_type=central_user_ops, is_preview=True, + ) as cmd_group: + cmd_group.command("create", "add_user") + with self.command_group( "iot central app device", command_type=central_device_ops, is_preview=True, ) as cmd_group: - cmd_group.command("list", "list_devices") + # cmd_group.command("list", "list_devices") cmd_group.command("show", "get_device") cmd_group.command("create", "create_device") cmd_group.command("delete", "delete_device") @@ -59,8 +66,8 @@ def load_central_commands(self, _): command_type=central_device_templates_ops, is_preview=True, ) as cmd_group: - cmd_group.command("list", "list_device_templates") - cmd_group.command("map", "map_device_templates") + # cmd_group.command("list", "list_device_templates") + # cmd_group.command("map", "map_device_templates") cmd_group.command("show", "get_device_template") cmd_group.command("create", "create_device_template") cmd_group.command("delete", "delete_device_template") diff --git a/azext_iot/central/commands_device_twin.py b/azext_iot/central/commands_device_twin.py index f5ce9fcb4..923ac11e7 100644 --- a/azext_iot/central/commands_device_twin.py +++ b/azext_iot/central/commands_device_twin.py @@ -9,8 +9,10 @@ from azext_iot.central.providers.devicetwin_provider import CentralDeviceTwinProvider -def device_twin_show(cmd, device_id, app_id, central_dns_suffix=CENTRAL_ENDPOINT): +def device_twin_show( + cmd, device_id, app_id, token=None, central_dns_suffix=CENTRAL_ENDPOINT +): device_twin_provider = CentralDeviceTwinProvider( - cmd=cmd, app_id=app_id, device_id=device_id + cmd=cmd, app_id=app_id, token=token, device_id=device_id ) return device_twin_provider.get_device_twin(central_dns_suffix=central_dns_suffix) diff --git a/azext_iot/central/commands_monitor.py b/azext_iot/central/commands_monitor.py index b7c9b1cf6..d3e297998 100644 --- a/azext_iot/central/commands_monitor.py +++ b/azext_iot/central/commands_monitor.py @@ -33,6 +33,7 @@ def validate_messages( duration=300, style="scroll", minimum_severity=Severity.warning.name, + token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): telemetry_args = TelemetryArguments( @@ -62,6 +63,7 @@ def validate_messages( provider = MonitorProvider( cmd=cmd, app_id=app_id, + token=token, consumer_group=consumer_group, central_dns_suffix=central_dns_suffix, central_handler_args=central_handler_args, @@ -80,6 +82,7 @@ def monitor_events( repair=False, properties=None, yes=False, + token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): telemetry_args = TelemetryArguments( @@ -109,6 +112,7 @@ def monitor_events( provider = MonitorProvider( cmd=cmd, app_id=app_id, + token=token, consumer_group=consumer_group, central_dns_suffix=central_dns_suffix, central_handler_args=central_handler_args, @@ -117,12 +121,13 @@ def monitor_events( def monitor_properties( - cmd, device_id, app_id, central_dns_suffix=CENTRAL_ENDPOINT, + cmd, device_id: str, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, ): monitor = PropertyMonitor( cmd=cmd, app_id=app_id, device_id=device_id, + token=token, central_dns_suffix=central_dns_suffix, ) monitor.start_property_monitor() @@ -130,8 +135,9 @@ def monitor_properties( def validate_properties( cmd, - device_id, - app_id, + device_id: str, + app_id: str, + token=None, central_dns_suffix=CENTRAL_ENDPOINT, minimum_severity=Severity.warning.name, ): @@ -139,6 +145,7 @@ def validate_properties( cmd=cmd, app_id=app_id, device_id=device_id, + token=token, central_dns_suffix=central_dns_suffix, ) monitor.start_validate_property_monitor(Severity[minimum_severity]) diff --git a/azext_iot/central/commands_user.py b/azext_iot/central/commands_user.py new file mode 100644 index 000000000..bdc076fbf --- /dev/null +++ b/azext_iot/central/commands_user.py @@ -0,0 +1,41 @@ +# 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. +# -------------------------------------------------------------------------------------------- +# Dev note - think of this as a controller + + +from azext_iot.constants import CENTRAL_ENDPOINT +from azext_iot.central.providers import CentralUserProvider +from azext_iot.central.models.enum import Role + + +def add_user( + cmd, + app_id: str, + assignee: str, + role: str, + email=None, + tenant_id=None, + object_id=None, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, +): + provider = CentralUserProvider(cmd=cmd, app_id=app_id, token=token) + + if email: + return provider.add_email( + assignee=assignee, + email=email, + role=Role[role], + central_dns_suffix=central_dns_suffix, + ) + + return provider.add_service_principal( + assignee=assignee, + tenant_id=tenant_id, + object_id=object_id, + role=Role[role], + central_dns_suffix=central_dns_suffix, + ) diff --git a/azext_iot/central/models/enum.py b/azext_iot/central/models/enum.py index 61dd1c345..1e4e976c5 100644 --- a/azext_iot/central/models/enum.py +++ b/azext_iot/central/models/enum.py @@ -21,3 +21,23 @@ class DeviceStatus(Enum): registered = "registered" blocked = "blocked" unassociated = "unassociated" + + +class Role(Enum): + """ + Types of roles a user can have in Central (admin, builder, etc) + """ + + admin = "ca310b8d-2f4a-44e0-a36e-957c202cd8d4" + builder = "344138e9-8de4-4497-8c54-5237e96d6aaf" + operator = "ae2c9854-393b-4f97-8c42-479d70ce626e" + + +class UserType(Enum): + """ + Types of users that can be added to use/manage a Central app + (service principal, email, etc) + """ + + service_principal = "ServicePrincipalUser" + email = "EmailUser" diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 5003f204c..23844232d 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -11,6 +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._params import event_msg_prop_type, event_timeout_type severity_type = CLIArgumentType( @@ -19,6 +20,12 @@ help="Minimum severity of issue required for reporting.", ) +role_type = CLIArgumentType( + options_list=["--role", "-r"], + choices=CaseInsensitiveList([role.name for role in Role]), + help="Role for the user/service-principal you are adding to the app.", +) + style_type = CLIArgumentType( options_list=["--style"], choices=CaseInsensitiveList(["scroll", "json", "csv"]), @@ -34,6 +41,7 @@ def load_central_arguments(self, _): with self.argument_context("iot central app") as context: context.argument("app_id", options_list=["--app-id", "-n"], help="Target App.") context.argument("minimum_severity", arg_type=severity_type) + context.argument("role", arg_type=role_type) context.argument( "instance_of", options_list=["--instance-of"], @@ -91,6 +99,26 @@ def load_central_arguments(self, _): context.argument( "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", ) + context.argument( + "assignee", + options_list=["--id", "--assignee"], + help="ID that will be associated with user being added to app. ", + ) + context.argument( + "email", + options_list=["--email"], + help="Email address of user to be added to the app. ", + ) + context.argument( + "object_id", + options_list=["--object-id", "--oid"], + help="Object ID for service principal to be added to the app. Tenant ID must also be specified. ", + ) + context.argument( + "tenant_id", + options_list=["--tenant-id", "--tnid"], + help="Tenant ID for service principal to be added to the app. Object ID must also be specified. ", + ) with self.argument_context("iot central app monitor-events") as context: context.argument("timeout", arg_type=event_timeout_type) @@ -170,6 +198,14 @@ def load_deprecated_iotcentral_params(self, _): help="Central dns suffix. " "This enables running cli commands against non public/prod environments", ) + context.argument( + "token", + options_list=["--token"], + help="Authorization token for request. " + "More info available here: https://docs.microsoft.com/en-us/learn/modules/manage-iot-central-apps-with-rest-api/ " + "MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...'). " + "Example: 'Bearer someBearerTokenHere'", + ) context.argument( "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", @@ -183,3 +219,11 @@ def load_deprecated_iotcentral_params(self, _): help="Central dns suffix. " "This enables running cli commands against non public/prod environments", ) + context.argument( + "token", + options_list=["--token"], + help="Authorization token for request. " + "More info available here: https://docs.microsoft.com/en-us/learn/modules/manage-iot-central-apps-with-rest-api/ " + "MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...'). " + "Example: 'Bearer someBearerTokenHere'", + ) diff --git a/azext_iot/central/providers/__init__.py b/azext_iot/central/providers/__init__.py index afd1eb98c..000212e37 100644 --- a/azext_iot/central/providers/__init__.py +++ b/azext_iot/central/providers/__init__.py @@ -9,9 +9,11 @@ CentralDeviceTemplateProvider, ) from azext_iot.central.providers.devicetwin_provider import CentralDeviceTwinProvider +from azext_iot.central.providers.user_provider import CentralUserProvider __all__ = [ "CentralDeviceProvider", "CentralDeviceTemplateProvider", "CentralDeviceTwinProvider", + "CentralUserProvider", ] diff --git a/azext_iot/central/providers/devicetwin_provider.py b/azext_iot/central/providers/devicetwin_provider.py index 98c0b0b21..51510f7ea 100644 --- a/azext_iot/central/providers/devicetwin_provider.py +++ b/azext_iot/central/providers/devicetwin_provider.py @@ -17,7 +17,7 @@ class CentralDeviceTwinProvider: - def __init__(self, cmd, app_id: str, device_id: str): + def __init__(self, cmd, app_id: str, token: str, device_id: str): """ Provider for devicetwin APIs @@ -31,12 +31,15 @@ def __init__(self, cmd, app_id: str, device_id: str): """ self._cmd = cmd self._app_id = app_id + self._token = token self._device_id = device_id def get_device_twin(self, central_dns_suffix): from azext_iot.common._azure import get_iot_central_tokens - tokens = get_iot_central_tokens(self._cmd, self._app_id, central_dns_suffix) + tokens = get_iot_central_tokens( + self._cmd, self._app_id, self._token, central_dns_suffix + ) exception = None diff --git a/azext_iot/central/providers/monitor_provider.py b/azext_iot/central/providers/monitor_provider.py index eb3bfd62a..555db570c 100644 --- a/azext_iot/central/providers/monitor_provider.py +++ b/azext_iot/central/providers/monitor_provider.py @@ -26,15 +26,21 @@ def __init__( self, cmd: AzCliCommand, app_id: str, + token: str, consumer_group: str, central_handler_args: CentralHandlerArguments, central_dns_suffix: str, ): - central_device_provider = CentralDeviceProvider(cmd, app_id) - central_template_provider = CentralDeviceTemplateProvider(cmd, app_id) + central_device_provider = CentralDeviceProvider( + cmd=cmd, app_id=app_id, token=token + ) + central_template_provider = CentralDeviceTemplateProvider( + cmd=cmd, app_id=app_id, token=token + ) self._targets = self._build_targets( cmd=cmd, app_id=app_id, + token=token, consumer_group=consumer_group, central_dns_suffix=central_dns_suffix, ) @@ -70,13 +76,14 @@ def _build_targets( self, cmd: AzCliCommand, app_id: str, + token: str, consumer_group: str, central_dns_suffix: str, ): from azext_iot.monitor.builders import central_target_builder targets = central_target_builder.build_central_event_hub_targets( - cmd, app_id, central_dns_suffix + cmd, app_id, token, central_dns_suffix ) [target.add_consumer_group(consumer_group) for target in targets] diff --git a/azext_iot/central/providers/user_provider.py b/azext_iot/central/providers/user_provider.py new file mode 100644 index 000000000..f5ccf1352 --- /dev/null +++ b/azext_iot/central/providers/user_provider.py @@ -0,0 +1,78 @@ +# 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 + + +logger = get_logger(__name__) + + +class CentralUserProvider: + 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, + ): + 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, + ) + + 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, + ) diff --git a/azext_iot/central/services/__init__.py b/azext_iot/central/services/__init__.py index 190ad31e8..b0c685e94 100644 --- a/azext_iot/central/services/__init__.py +++ b/azext_iot/central/services/__init__.py @@ -4,7 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_iot.central.services import device, device_template +from azext_iot.central.services import device, device_template, user -__all__ = ["device", "device_template"] +__all__ = ["device", "device_template", "user"] diff --git a/azext_iot/central/services/user.py b/azext_iot/central/services/user.py new file mode 100644 index 000000000..2502ffcc0 --- /dev/null +++ b/azext_iot/central/services/user.py @@ -0,0 +1,93 @@ +# 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 requests + +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 + +logger = get_logger(__name__) + +BASE_PATH = "api/preview/users" + + +def add_service_principal( + cmd, + app_id: str, + assignee: str, + tenant_id: str, + object_id: str, + role: Role, + token: str, + central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + Add a user to a Central app + + Args: + cmd: command passed into az + tenant_id: tenant id of service principal to be added + object_id: object id of service principal to be added + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + user: dict + """ + url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, assignee) + + payload = { + "tenantId": tenant_id, + "objectId": object_id, + "type": UserType.service_principal.value, + "roles": [{"role": role.value}], + } + + headers = _utility.get_headers(token, cmd, has_json_payload=True) + + response = requests.put(url, headers=headers, json=payload) + return _utility.try_extract_result(response) + + +def add_email( + cmd, + app_id: str, + assignee: str, + email: str, + role: Role, + token: str, + central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + Add a user to a Central app + + Args: + cmd: command passed into az + email: email of user to be added + app_id: name of app (used for forming request URL) + token: (OPTIONAL) authorization token to fetch device details from IoTC. + MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + user: dict + """ + url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, assignee) + + payload = { + "email": email, + "type": UserType.email.value, + "roles": [{"role": role.value}], + } + + headers = _utility.get_headers(token, cmd, has_json_payload=True) + + response = requests.put(url, headers=headers, json=payload) + return _utility.try_extract_result(response) diff --git a/azext_iot/common/_azure.py b/azext_iot/common/_azure.py index c759095f4..ffa76293c 100644 --- a/azext_iot/common/_azure.py +++ b/azext_iot/common/_azure.py @@ -119,19 +119,19 @@ def _find_iot_dps_from_list(all_dps, dps_name): return result -def get_iot_central_tokens(cmd, app_id, central_dns_suffix): +def get_iot_central_tokens(cmd, app_id, token, central_dns_suffix): import requests - aad_token = get_aad_token(cmd, resource="https://apps.azureiotcentral.com")[ - "accessToken" - ] + if not token: + aad_token = get_aad_token(cmd, resource="https://apps.azureiotcentral.com")[ + "accessToken" + ] + token = "Bearer {}".format(aad_token) url = "https://{}.{}/system/iothubs/generateSasTokens".format( app_id, central_dns_suffix ) - response = requests.post( - url, headers={"Authorization": "Bearer {}".format(aad_token)} - ) + response = requests.post(url, headers={"Authorization": token}) tokens = response.json() additional_help = ( diff --git a/azext_iot/monitor/builders/central_target_builder.py b/azext_iot/monitor/builders/central_target_builder.py index 3f941b4b3..84c8ca644 100644 --- a/azext_iot/monitor/builders/central_target_builder.py +++ b/azext_iot/monitor/builders/central_target_builder.py @@ -12,15 +12,21 @@ from azext_iot.monitor.builders._common import convert_token_to_target -def build_central_event_hub_targets(cmd, app_id, central_dns_suffix) -> List[Target]: +def build_central_event_hub_targets( + cmd, app_id, aad_token, central_dns_suffix +) -> List[Target]: event_loop = asyncio.get_event_loop() return event_loop.run_until_complete( - _build_central_event_hub_targets_async(cmd, app_id, central_dns_suffix) + _build_central_event_hub_targets_async( + cmd, app_id, aad_token, central_dns_suffix + ) ) -async def _build_central_event_hub_targets_async(cmd, app_id, central_dns_suffix): - all_tokens = get_iot_central_tokens(cmd, app_id, central_dns_suffix) +async def _build_central_event_hub_targets_async( + cmd, app_id, aad_token, central_dns_suffix +): + all_tokens = get_iot_central_tokens(cmd, app_id, aad_token, central_dns_suffix) targets = [await convert_token_to_target(token) for token in all_tokens.values()] return targets diff --git a/azext_iot/monitor/property.py b/azext_iot/monitor/property.py index ed694a053..3c78e3d88 100644 --- a/azext_iot/monitor/property.py +++ b/azext_iot/monitor/property.py @@ -27,18 +27,29 @@ class PropertyMonitor: def __init__( - self, cmd, app_id, device_id, central_dns_suffix=CENTRAL_ENDPOINT, + self, + cmd, + app_id: str, + device_id: str, + token: str, + central_dns_suffix=CENTRAL_ENDPOINT, ): self._cmd = cmd self._app_id = app_id self._device_id = device_id + self._token = token self._central_dns_suffix = central_dns_suffix self._device_twin_provider = CentralDeviceTwinProvider( - cmd=self._cmd, app_id=self._app_id, device_id=self._device_id + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + device_id=self._device_id, + ) + self._central_device_provider = CentralDeviceProvider( + cmd=self._cmd, app_id=self._app_id, token=self._token ) - self._central_device_provider = CentralDeviceProvider(self._cmd, self._app_id) self._central_template_provider = CentralDeviceTemplateProvider( - cmd=self._cmd, app_id=self._app_id + cmd=self._cmd, app_id=self._app_id, token=self._token ) self._template = self._get_device_template() @@ -146,7 +157,6 @@ def _is_interface(self, interface_name): return interface_name_modified in self._template.interfaces 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, @@ -158,7 +168,6 @@ def start_property_monitor(self,): prev_twin = None while True: - raw_twin = self._device_twin_provider.get_device_twin( central_dns_suffix=self._central_dns_suffix ) @@ -181,6 +190,7 @@ def start_property_monitor(self,): print("Changes in reported properties:") print("version :", twin.reported_property.version) print(change_r) + time.sleep(DEVICETWIN_POLLING_INTERVAL_SEC) prev_twin = twin diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index cceea463c..7f8ef26b6 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -111,7 +111,7 @@ def test_central_monitor_events(self): # Test with invalid app-id self.cmd( - "iotcentral app monitor-events --app-id {}".format(APP_ID + "zzz"), + "iotcentral app monitor-events --app-id {} -y".format(APP_ID + "zzz"), expect_failure=True, ) @@ -226,7 +226,7 @@ def test_central_validate_messages_issues_detected(self): for issue in expected_issues: assert issue in output - def test_central_device_methods_CRLD(self): + def test_central_device_methods_CRD(self): (device_id, device_name) = self._create_device() self.cmd( @@ -239,13 +239,9 @@ def test_central_device_methods_CRLD(self): ], ) - list_output = self.cmd("iot central app device list --app-id {}".format(APP_ID)) - self._delete_device(device_id) - assert device_id in list_output.get_output_in_json() - - def test_central_device_template_methods_CRLD(self): + def test_central_device_template_methods_CRD(self): # currently: create, show, list, delete (template_id, template_name) = self._create_device_template() @@ -259,20 +255,8 @@ def test_central_device_template_methods_CRLD(self): ], ) - list_output = self.cmd( - "iot central app device-template list --app-id {}".format(APP_ID) - ) - map_output = self.cmd( - "iot central app device-template map --app-id {}".format(APP_ID) - ) - self._delete_device_template(template_id) - assert template_id in list_output.get_output_in_json() - - map_json = map_output.get_output_in_json() - assert map_json[template_name] == template_id - def test_central_device_registration_info_registered(self): (template_id, _) = self._create_device_template() (device_id, device_name) = self._create_device( @@ -326,7 +310,8 @@ def test_central_run_command(self): " -d {}" " -i {}" " --cn {}" - " -k '{}'".format( + " -k '{}'" + "".format( APP_ID, device_id, interface_id, command_name, sync_command_params ) ) @@ -336,7 +321,8 @@ def test_central_run_command(self): " -n {}" " -d {}" " -i {}" - " --cn {}".format(APP_ID, device_id, interface_id, command_name) + " --cn {}" + "".format(APP_ID, device_id, interface_id, command_name) ) self._delete_device(device_id) @@ -393,7 +379,7 @@ def test_central_device_registration_info_unassociated(self): def test_central_device_registration_summary(self): result = self.cmd( - "iot central app device registration-summary --app-id {}".format(APP_ID,) + "iot central app device registration-summary --app-id {}".format(APP_ID) ) json_result = result.get_output_in_json() @@ -476,13 +462,19 @@ def _create_device_template(self): return (template_id, template_name) def _delete_device_template(self, template_id): - self.cmd( - "iot central app device-template delete --app-id {} --device-template-id {}".format( - APP_ID, template_id - ), - checks=[self.check("result", "success")], + attempts = range(0, 10) + command = "iot central app device-template delete --app-id {} --device-template-id {}".format( + APP_ID, template_id ) + # retry logic to delete the template + for _ in attempts: + try: + self.cmd(command, checks=[self.check("result", "success")]) + return + except: + time.sleep(10) + def _get_credentials(self, device_id): return self.cmd( "iot central app device show-credentials --app-id {} -d {}".format( @@ -513,7 +505,7 @@ def _get_monitor_events_output(self, device_id, enqueued_time, asserts=None): asserts = [] output = self.command_execute_assert( - "iot central app monitor-events -n {} -d {} --et {} --to 1".format( + "iot central app monitor-events -n {} -d {} --et {} --to 1 -y".format( APP_ID, device_id, enqueued_time ), asserts, diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index 0ca9b0aa8..fa6b6529a 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -114,7 +114,7 @@ class Cmd: # Test to ensure get_iot_central_tokens calls requests.post and tokens are returned assert ( - get_iot_central_tokens(Cmd(), "app_id", "api-uri").value() + get_iot_central_tokens(Cmd(), "app_id", "", "api-uri").value() == "fixture_requests_post value" ) @@ -225,7 +225,11 @@ def test_should_return_updated_properties( twin_next = DeviceTwin(raw_twin) twin_next.reported_property.version = twin.reported_property.version + 1 monitor = PropertyMonitor( - cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + cmd=None, + app_id=app_id, + device_id=device_id, + token=None, + central_dns_suffix=None, ) result = monitor._compare_properties( twin_next.reported_property, twin.reported_property @@ -263,7 +267,11 @@ def test_should_return_no_properties( twin = DeviceTwin(raw_twin) twin_next = DeviceTwin(raw_twin) monitor = PropertyMonitor( - cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + cmd=None, + app_id=app_id, + device_id=device_id, + token=None, + central_dns_suffix=None, ) result = monitor._compare_properties( twin_next.reported_property, twin.reported_property @@ -282,7 +290,11 @@ def test_validate_properties_declared_multiple_interfaces( ) monitor = PropertyMonitor( - cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + cmd=None, + app_id=app_id, + device_id=device_id, + token=None, + central_dns_suffix=None, ) model = {"Model": "test_model"} @@ -318,7 +330,11 @@ def test_validate_properties_name_miss( ) monitor = PropertyMonitor( - cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + cmd=None, + app_id=app_id, + device_id=device_id, + token=None, + central_dns_suffix=None, ) # invalid interface / property @@ -371,7 +387,11 @@ def test_validate_properties_severity_level( ) monitor = PropertyMonitor( - cmd=None, app_id=app_id, device_id=device_id, central_dns_suffix=None, + cmd=None, + app_id=app_id, + device_id=device_id, + token=None, + central_dns_suffix=None, ) # severity level info From 2e23617c00c3e6e9caa846b2c23d14e1767ffccd Mon Sep 17 00:00:00 2001 From: prbans <42591144+prbans@users.noreply.github.com> Date: Tue, 21 Jul 2020 17:05:13 -0700 Subject: [PATCH 067/179] Remove deprecated commands (#219) --- azext_iot/central/_help.py | 94 ------------------------- azext_iot/central/command_map.py | 41 ----------- azext_iot/central/params.py | 85 ---------------------- azext_iot/tests/test_iot_central_int.py | 24 +------ 4 files changed, 1 insertion(+), 243 deletions(-) diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 8244a39bc..7f7822d2e 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -33,9 +33,6 @@ def load_central_help(): _load_central_device_twin_help() _load_central_monitors_help() - # TODO: Delete this by end of July 2020 - _load_central_deprecated_commands() - def _load_central_devices_help(): helps[ @@ -400,94 +397,3 @@ def _load_central_monitors_help(): text: > az iot central app validate-properties --app-id {app_id} -d {device_id} """ - - -# TODO: Delete this by July 2020 -def _load_central_deprecated_commands(): - helps[ - "iotcentral app monitor-events" - ] = """ - type: command - short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. - long-summary: | - EXPERIMENTAL requires Python 3.5+ - This command relies on and may install dependent Cython package (uamqp) upon first execution. - https://github.com/Azure/azure-uamqp-python - - DEPRECATED. Use 'az iot central app monitor-events' instead. - examples: - - name: Basic usage - text: > - az iotcentral app monitor-events --app-id {app_id} - - name: Basic usage when filtering on target device - text: > - az iotcentral app monitor-events --app-id {app_id} -d {device_id} - - name: Basic usage when filtering targeted devices with a wildcard in the ID - text: > - az iotcentral app monitor-events --app-id {app_id} -d Device* - - name: Basic usage when filtering on module. - text: > - az iotcentral app monitor-events --app-id {app_id} -m {module_id} - - name: Basic usage when filtering targeted modules with a wildcard in the ID - text: > - az iotcentral app monitor-events --app-id {app_id} -m Module* - - name: Filter device and specify an Event Hub consumer group to bind to. - text: > - az iotcentral app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} - - name: Receive message annotations (message headers) - text: > - az iotcentral app monitor-events --app-id {app_id} -d {device_id} --properties anno - - name: Receive message annotations + system properties. Never time out. - text: > - az iotcentral app monitor-events --app-id {app_id} -d {device_id} --properties anno sys --timeout 0 - - name: Receive all message attributes from all device messages - text: > - az iotcentral app monitor-events --app-id {app_id} --props all - - name: Receive all messages and parse message payload as JSON - text: > - az iotcentral app monitor-events --app-id {app_id} --output json - """ - - helps[ - "iotcentral device-twin" - ] = """ - type: group - short-summary: Manage IoT Central device twins. - long-summary: DEPRECATED. Use 'az iot central device-twin' instead. - """ - - helps[ - "iotcentral device-twin show" - ] = """ - type: command - short-summary: Get the device twin from IoT Hub. - long-summary: DEPRECATED. Use 'az iot central device-twin show' instead. - """ - - helps[ - "iot central device-twin" - ] = """ - type: group - short-summary: Manage IoT Central device twins. - """ - - helps[ - "iot central device-twin show" - ] = """ - type: command - short-summary: Get the device twin from IoT Hub. - """ - - helps[ - "iotcentral app device-twin" - ] = """ - type: group - short-summary: Manage IoT Central device twins. - """ - - helps[ - "iotcentral app device-twin show" - ] = """ - type: command - short-summary: Manage IoT Central device twins. - """ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index 81980ce06..d50d63d7c 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -76,44 +76,3 @@ def load_central_commands(self, _): "iot central app device-twin", command_type=central_device_twin_ops ) as cmd_group: cmd_group.command("show", "device_twin_show") - - # TODO: Delete this by end of July 2020 - load_deprecated_commands(self, _) - - -# TODO: Delete this by end of July 2020 -def load_deprecated_commands(self, _): - with self.command_group( - "iotcentral", - command_type=central_monitor_ops, - deprecate_info=self.deprecate(redirect="iot central"), - ) as _: - pass - - with self.command_group( - "iotcentral app", - command_type=central_monitor_ops, - deprecate_info=self.deprecate(redirect="iot central app"), - ) as cmd_group: - cmd_group.command("monitor-events", "monitor_events") - - with self.command_group( - "iotcentral device-twin", - command_type=central_device_twin_ops, - deprecate_info=self.deprecate(redirect="iot central app device-twin"), - ) as cmd_group: - cmd_group.command("show", "device_twin_show") - - with self.command_group( - "iotcentral app device-twin", - command_type=central_device_twin_ops, - deprecate_info=self.deprecate(redirect="iot central app device-twin"), - ) as cmd_group: - cmd_group.command("show", "device_twin_show") - - with self.command_group( - "iot central device-twin", - command_type=central_device_twin_ops, - deprecate_info=self.deprecate(redirect="iot central app device-twin"), - ) as cmd_group: - cmd_group.command("show", "device_twin_show") diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 23844232d..2efd7c892 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -142,88 +142,3 @@ def load_central_arguments(self, _): help="Maximum number of messages to recieve from target device before terminating connection." "Use 0 for infinity.", ) - # TODO: Delete this by end of July 2020 - load_deprecated_iotcentral_params(self, _) - - -# TODO: Delete this by end of July 2020 -def load_deprecated_iotcentral_params(self, _): - with self.argument_context("iotcentral") as context: - context.argument("app_id", options_list=["--app-id", "-n"], help="Target App.") - context.argument("properties", arg_type=event_msg_prop_type) - context.argument("timeout", arg_type=event_timeout_type) - context.argument( - "device_id", options_list=["--device-id", "-d"], help="Target Device." - ) - context.argument( - "timeout", - options_list=["--timeout", "--to", "-t"], - type=int, - help="Maximum seconds to maintain connection. Use 0 for infinity. ", - ) - context.argument( - "consumer_group", - options_list=["--consumer-group", "--cg", "-c"], - help="Specify the consumer group to use when connecting to event hub endpoint.", - ) - context.argument( - "enqueued_time", - options_list=["--enqueued-time", "--et", "-e"], - type=int, - help="Indicates the time that should be used as a starting point to read messages from the partitions. " - "Units are milliseconds since unix epoch. " - 'If no time is indicated "now" is used.', - ) - context.argument( - "content_type", - options_list=["--content-type", "--ct"], - help="Specify the Content-Type of the message payload to automatically format the output to that type.", - ) - context.argument( - "repair", - options_list=["--repair", "-r"], - arg_type=get_three_state_flag(), - help="Reinstall uamqp dependency compatible with extension version. Default: false", - ) - context.argument( - "yes", - options_list=["--yes", "-y"], - arg_type=get_three_state_flag(), - help="Skip user prompts. Indicates acceptance of dependency installation (if required). " - "Used primarily for automation scenarios. Default: false", - ) - context.argument( - "central_dns_suffix", - options_list=["--central-dns-suffix", "--central-api-uri"], - help="Central dns suffix. " - "This enables running cli commands against non public/prod environments", - ) - context.argument( - "token", - options_list=["--token"], - help="Authorization token for request. " - "More info available here: https://docs.microsoft.com/en-us/learn/modules/manage-iot-central-apps-with-rest-api/ " - "MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...'). " - "Example: 'Bearer someBearerTokenHere'", - ) - - context.argument( - "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", - ) - - with self.argument_context("iot central device-twin") as context: - context.argument("app_id", options_list=["--app-id", "-n"], help="Target App.") - context.argument( - "central_dns_suffix", - options_list=["--central-dns-suffix", "--central-api-uri"], - help="Central dns suffix. " - "This enables running cli commands against non public/prod environments", - ) - context.argument( - "token", - options_list=["--token"], - help="Authorization token for request. " - "More info available here: https://docs.microsoft.com/en-us/learn/modules/manage-iot-central-apps-with-rest-api/ " - "MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...'). " - "Example: 'Bearer someBearerTokenHere'", - ) diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 7f8ef26b6..815322ca6 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -36,21 +36,6 @@ def __init__(self, test_scenario): def test_central_device_twin_show_fail(self): (device_id, _) = self._create_device() - # Verify incorrect app-id throws error - self.cmd( - "iotcentral device-twin show --app-id incorrect-app --device-id {}".format( - device_id - ), - expect_failure=True, - ) - # Verify incorrect device-id throws error - self.cmd( - "iotcentral device-twin show --app-id {} --device-id incorrect-device".format( - APP_ID - ), - expect_failure=True, - ) - # Verify incorrect app-id throws error self.cmd( "iot central app device-twin show --app-id incorrect-app --device-id {}".format( @@ -75,13 +60,6 @@ def test_central_device_twin_show_success(self): # wait about a few seconds for simulator to kick in so that provisioning completes time.sleep(60) - self.cmd( - "iotcentral device-twin show --app-id {} --device-id {}".format( - APP_ID, device_id - ), - checks=[self.check("deviceId", device_id)], - ) - self.cmd( "iot central app device-twin show --app-id {} --device-id {}".format( APP_ID, device_id @@ -111,7 +89,7 @@ def test_central_monitor_events(self): # Test with invalid app-id self.cmd( - "iotcentral app monitor-events --app-id {} -y".format(APP_ID + "zzz"), + "iot central app monitor-events --app-id {} -y".format(APP_ID + "zzz"), expect_failure=True, ) From b08db0c17bd7ab27722b660d2fdbc715fde6ddf2 Mon Sep 17 00:00:00 2001 From: valluriraj Date: Thu, 23 Jul 2020 15:29:29 -0700 Subject: [PATCH 068/179] validate-messages enhancements (#220) * validate-messages enhancements * add aka.ms link * address review comments --- azext_iot/monitor/parsers/strings.py | 8 +++----- azext_iot/tests/test_iot_central_int.py | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/azext_iot/monitor/parsers/strings.py b/azext_iot/monitor/parsers/strings.py index f62668f9a..9c3450a1c 100644 --- a/azext_iot/monitor/parsers/strings.py +++ b/azext_iot/monitor/parsers/strings.py @@ -84,11 +84,9 @@ def duplicate_property_name(duplicate_prop_name, interfaces: list): # error def invalid_primitive_schema_mismatch_template(field_name: str, data_type: str, data): return ( - "Datatype of field '{}' does not match the datatype '{}'. Data '{}'. " - "All dates/times/datetimes/durations must be ISO 8601 compliant.".format( - field_name, data_type, data, - ) - ) + "Datatype of telemetry field '{}' does not match the datatype {}. Data sent by the device : {}. " + "For more information, see: https://aka.ms/iotcentral-payloads" + ).format(field_name, data_type, data,) # to remove diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 815322ca6..c54e0ca79 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -196,8 +196,9 @@ def test_central_validate_messages_issues_detected(self): expected_issues = [ "No encoding found. Expected encoding 'utf-8' to be present in message header.", "Content type '' is not supported. Expected Content type is 'application/json'.", - "Datatype of field 'Bool' does not match the datatype 'boolean'.", - "Data '123'. All dates/times/datetimes/durations must be ISO 8601 compliant.", + "Datatype of telemetry field 'Bool' does not match the datatype boolean.", + "Data sent by the device : 123.", + "For more information, see: https://aka.ms/iotcentral-payloads", "Following capabilities have NOT been defined in the device template '['NotPresentInTemplate']'", "Invalid JSON format", ] From 1dad067b6d8e5b6c3f606f58e95d4fa47ec146ba Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 28 Jul 2020 09:28:26 -0700 Subject: [PATCH 069/179] CI + misc improvements (#221) * Supports pipeline improvements * Full consistent IT battery run and success for release pipeline * Refactors and improvements of the merge pipeline * Decouples from Azure CLI tools directory * Runs CLI command table checks and validation at medium min severity * +Credscan job * Simplifies IT setup - removed need of IoT Hub connection string + removes a number of previously required ADT test vars. * Improvement for DT create operation. location is no longer required. If no location is provided the resource group location is used. * Increases min cli version to 2.3.1 and clean-up deprecated IotHub client factory path. --- .azure-devops/create-release.yml | 13 +- .azure-devops/merge.yml | 88 +++++---- .../build-publish-azure-cli-test-sdk.yml | 10 +- .../build-publish-azure-iot-cli-extension.yml | 4 - .azure-devops/templates/install-azdev.yml | 2 +- .../templates/install-configure-azure-cli.yml | 14 ++ .azure-devops/templates/run-all-tests.yml | 8 - .azure-devops/templates/run-tests.yml | 23 ++- .azure-devops/templates/set-pythonpath.yml | 9 +- .../templates/set-testenv-sentinel.yml | 24 ++- azext_iot/_factory.py | 9 +- azext_iot/azext_metadata.json | 4 +- azext_iot/constants.py | 2 +- azext_iot/digitaltwins/commands_resource.py | 2 +- azext_iot/digitaltwins/params.py | 2 +- azext_iot/digitaltwins/providers/resource.py | 7 +- azext_iot/tests/__init__.py | 17 +- azext_iot/tests/digitaltwins/__init__.py | 73 +++++--- .../test_dt_model_lifecycle_int.py | 45 ++--- .../test_dt_resource_lifecycle_int.py | 175 ++++++++---------- .../test_dt_twin_lifecycle_int.py | 171 ++++++++--------- .../configurations/test_iot_config_int.py | 43 +++-- .../tests/iothub/device_identity/__init__.py | 5 - .../test_iot_device_identity_int.py | 22 --- .../tests/iothub/jobs/test_iothub_jobs_int.py | 9 +- azext_iot/tests/settings.py | 10 +- azext_iot/tests/test_iot_ext_int.py | 85 +++++---- azext_iot/tests/test_iot_messaging_int.py | 96 +++++----- linter_exclusions.yml | 15 ++ 29 files changed, 485 insertions(+), 502 deletions(-) create mode 100644 .azure-devops/templates/install-configure-azure-cli.yml delete mode 100644 .azure-devops/templates/run-all-tests.yml delete mode 100644 azext_iot/tests/iothub/device_identity/__init__.py delete mode 100644 azext_iot/tests/iothub/device_identity/test_iot_device_identity_int.py create mode 100644 linter_exclusions.yml diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index d7aadd474..4a6ee7a71 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -4,6 +4,11 @@ trigger: none jobs: + # track deployment in environments +- deployment: 'StageGitHub' + displayName: 'Stage CLI extension on GitHub' + environment: 'production' + - job: 'Build_Publish_Azure_IoT_CLI_Extension' pool: vmImage: 'Ubuntu-16.04' @@ -11,7 +16,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7.x' + versionSpec: '3.6.x' architecture: 'x64' - template: templates/setup-ci-machine.yml @@ -25,7 +30,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7.x' + versionSpec: '3.6.x' architecture: 'x64' - template: templates/setup-ci-machine.yml @@ -40,7 +45,7 @@ jobs: steps: - template: templates/run-tests.yml parameters: - pythonVersion: '3.7.x' + pythonVersion: '3.6.x' runUnitTestsOnly: 'false' - script: 'pip install .' @@ -91,7 +96,7 @@ jobs: - task: GitHubRelease@0 inputs: - gitHubConnection: GitHub + gitHubConnection: GitHubP repositoryName: $(Build.Repository.Name) action: 'create' target: '$(Build.SourceVersion)' diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 484ac9b11..5ade5c845 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -8,63 +8,38 @@ pr: variables: iot_ext_package: azure-iot + iot_ext_venv: venv jobs: -- job: 'Build_Publish_Azure_IoT_CLI_Extension' +- job: 'build_and_publish_azure_iot_cli_ext' pool: vmImage: 'Ubuntu-16.04' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7.x' + versionSpec: '3.6.x' architecture: 'x64' - template: templates/setup-ci-machine.yml - template: templates/build-publish-azure-iot-cli-extension.yml -- job: 'Build_Publish_Azure_CLI_Test_SDK' +- job: 'build_and_publish_azure_cli_test_sdk' pool: vmImage: 'Ubuntu-16.04' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7.x' + versionSpec: '3.6.x' architecture: 'x64' - template: templates/setup-ci-machine.yml - template: templates/build-publish-azure-cli-test-sdk.yml -- job: 'Run_Tests_Windows' - dependsOn : [ 'Build_Publish_Azure_CLI_Test_SDK', 'Build_Publish_Azure_IoT_CLI_Extension'] - pool: - vmImage: 'vs2017-win2016' - - steps: - - task: PowerShell@2 - inputs: - targetType: 'inline' - script : 'ren "C:\Program Files\Common Files\AzureCliExtensionDirectory" "C:\Program Files\Common Files\AzureCliExtensionDirectory1"' - - - template: templates/run-tests.yml - parameters: - pythonVersion: '3.x' - -- job: 'Run_Tests_Windows_Azure_CLI_Released_Version' - dependsOn : [Build_Publish_Azure_CLI_Test_SDK, 'Build_Publish_Azure_IoT_CLI_Extension'] - pool: - vmImage: 'vs2017-win2016' - - steps: - - template: templates/run-tests.yml - parameters: - pythonVersion: '3.x' - runWithAzureCliReleased: 'false' - -- job: 'Run_Tests_Ubuntu' - dependsOn: ['Build_Publish_Azure_CLI_Test_SDK','Build_Publish_Azure_IoT_CLI_Extension'] +- 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' strategy: @@ -75,7 +50,7 @@ jobs: python.version: '3.7.x' Python38: python.version: '3.8.x' - maxParallel: 4 + maxParallel: 3 steps: - bash: sudo rm -R -f /usr/local/lib/azureExtensionDir @@ -84,8 +59,8 @@ jobs: parameters: pythonVersion: '$(python.version)' -- job: 'Run_Tests_Mac' - dependsOn: ['Build_Publish_Azure_CLI_Test_SDK','Build_Publish_Azure_IoT_CLI_Extension'] +- job: 'run_unit_tests_macOs' + dependsOn: ['build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] pool: vmImage: 'macOS-10.14' @@ -94,8 +69,23 @@ jobs: parameters: pythonVersion: '3.x' -- job: 'Run_Style_Check' - dependsOn: ['Build_Publish_Azure_CLI_Test_SDK','Build_Publish_Azure_IoT_CLI_Extension'] +- job: 'run_unit_tests_windows' + dependsOn : [ 'build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] + pool: + vmImage: 'vs2017-win2016' + + steps: + - task: PowerShell@2 + inputs: + targetType: 'inline' + script : 'ren "C:\Program Files\Common Files\AzureCliExtensionDirectory" "C:\Program Files\Common Files\AzureCliExtensionDirectory1"' + + - template: templates/run-tests.yml + parameters: + pythonVersion: '3.x' + +- job: 'run_style_check' + dependsOn: ['build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] pool: vmImage: 'Ubuntu-16.04' @@ -117,8 +107,9 @@ jobs: displayName: 'Evaluate with flake8' workingDirectory: '.' -- job: 'Run_Core_Linter_and_HelpText_Check' - dependsOn: ['Build_Publish_Azure_CLI_Test_SDK','Build_Publish_Azure_IoT_CLI_Extension'] +# TODO: Evaluate this style or similar alternative for setting up CLI env +- job: 'run_azdev_linter_on_command_table' + displayName: 'Evaluate IoT extension command table' pool: vmImage: 'Ubuntu-16.04' @@ -128,10 +119,17 @@ jobs: versionSpec: '3.x' architecture: 'x64' - - template: templates/install-azure-cli-released.yml - - template: templates/install-azdev.yml - - template: templates/setup-ci-machine.yml # Likely temporary fix due to pkg_resources.ContextualVersionConflict from six ver 1.13.0 vs pinned 1.12.0 + - template: templates/install-configure-azure-cli.yml - template: templates/download-install-local-azure-iot-cli-extension.yml - - - script: 'azdev cli-lint --ci --extensions $(iot_ext_package)' - displayName: 'Evaluate with CLI core linter' + - bash: | + set -ev + source ./$(iot_ext_venv)/bin/activate + cp ./linter_exclusions.yml $AZURE_EXTENSION_DIR/$(iot_ext_package)/ + azdev linter --include-whl-extensions $(iot_ext_package) --min-severity medium + +- job: CredScan + displayName: "Credential Scan" + steps: + - task: CredScan@3 + inputs: + scanFolder: '$(Build.SourcesDirectory)' diff --git a/.azure-devops/templates/build-publish-azure-cli-test-sdk.yml b/.azure-devops/templates/build-publish-azure-cli-test-sdk.yml index f797cdf21..263108a86 100644 --- a/.azure-devops/templates/build-publish-azure-cli-test-sdk.yml +++ b/.azure-devops/templates/build-publish-azure-cli-test-sdk.yml @@ -1,21 +1,21 @@ steps: - - script: 'rm -rf azure-cli' + - script: 'rm -rf ../azure-cli' displayName: 'delete azure cli directory' - - script: 'git clone https://github.com/Azure/azure-cli.git' + - script: 'git clone -q --single-branch -b master https://github.com/Azure/azure-cli.git ../azure-cli' displayName: 'Clone Azure CLI repository' - script: 'pip install --upgrade .' displayName: 'Install Azure CLI test SDK' - workingDirectory: 'azure-cli/src/azure-cli-testsdk/' + workingDirectory: '../azure-cli/src/azure-cli-testsdk/' - script: 'python setup.py sdist bdist_wheel' displayName: 'Build wheel for Azure CLI test SDK' - workingDirectory: 'azure-cli/src/azure-cli-testsdk/' + workingDirectory: '../azure-cli/src/azure-cli-testsdk/' - task: PublishBuildArtifacts@1 displayName: 'Publish Azure CLI test SDK as artifact' inputs: - pathtoPublish: 'azure-cli/src/azure-cli-testsdk/dist' + pathtoPublish: '../azure-cli/src/azure-cli-testsdk/dist' artifactName: 'azure-cli-test-sdk' publishLocation: 'Container' diff --git a/.azure-devops/templates/build-publish-azure-iot-cli-extension.yml b/.azure-devops/templates/build-publish-azure-iot-cli-extension.yml index 4a41bde21..b9af22ad6 100644 --- a/.azure-devops/templates/build-publish-azure-iot-cli-extension.yml +++ b/.azure-devops/templates/build-publish-azure-iot-cli-extension.yml @@ -1,8 +1,4 @@ steps: - - script: 'pip install --upgrade .' - displayName: 'Install Azure IoT CLI extension' - workingDirectory: '.' - - script: 'python setup.py sdist bdist_wheel' displayName: 'Build wheel for Azure IoT CLI extension' workingDirectory: '.' diff --git a/.azure-devops/templates/install-azdev.yml b/.azure-devops/templates/install-azdev.yml index c58c5a447..b3efc8160 100644 --- a/.azure-devops/templates/install-azdev.yml +++ b/.azure-devops/templates/install-azdev.yml @@ -1,3 +1,3 @@ steps: - - script: 'pip install -e "git+https://github.com/Azure/azure-cli@dev#egg=azure-cli-dev-tools&subdirectory=tools"' + - script: 'pip install azdev' displayName: 'Install azdev tool' diff --git a/.azure-devops/templates/install-configure-azure-cli.yml b/.azure-devops/templates/install-configure-azure-cli.yml new file mode 100644 index 000000000..e23a11e99 --- /dev/null +++ b/.azure-devops/templates/install-configure-azure-cli.yml @@ -0,0 +1,14 @@ +steps: + - task: Bash@3 + inputs: + targetType: 'inline' + script: | + set -ev + pip install virtualenv + python -m virtualenv $(iot_ext_venv)/ + source ./$(iot_ext_venv)/bin/activate + git clone --single-branch -b master https://github.com/Azure/azure-cli.git ../azure-cli + pip install azdev + azdev --version + azdev setup -c ../azure-cli + AZURE_EXTENSION_DIR=~/.azure/cliextensions diff --git a/.azure-devops/templates/run-all-tests.yml b/.azure-devops/templates/run-all-tests.yml deleted file mode 100644 index e31b8b16d..000000000 --- a/.azure-devops/templates/run-all-tests.yml +++ /dev/null @@ -1,8 +0,0 @@ -steps: -- task: AzureCLI@2 - displayName: 'Run all tests' - inputs: - azureSubscription: AzIoTCLIService - scriptType: bash - scriptLocation: inlineScript - inlineScript: pytest -vv -rf azext_iot/tests/ --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-full-results.xml diff --git a/.azure-devops/templates/run-tests.yml b/.azure-devops/templates/run-tests.yml index 4fa07db98..6fa49454d 100644 --- a/.azure-devops/templates/run-tests.yml +++ b/.azure-devops/templates/run-tests.yml @@ -23,15 +23,30 @@ steps: - template: set-pythonpath.yml + - template: set-testenv-sentinel.yml + - ${{ if eq(parameters.runUnitTestsOnly, 'false') }}: - - template: set-testenv-sentinel.yml - - template: run-all-tests.yml + + - task: AzureCLI@2 + displayName: 'Execute full IoT extension test run' + inputs: + azureSubscription: AzIoTCLIService + scriptType: bash + scriptLocation: inlineScript + inlineScript: pytest -vv -r azext_iot/tests/ --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-full-results.xml --cov-report=xml - ${{ if eq(parameters.runUnitTestsOnly, 'true') }}: - - template: set-testenv-sentinel.yml - - script: pytest -vv azext_iot/tests/ -k "_unit" --junitxml=junit/test-iotext-unit-results.xml + + - script: pytest -vv -r azext_iot/tests/ -k "_unit" --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-unit-results.xml --cov-report=xml displayName: 'Execute IoT extension unit tests' + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: 'cobertura' + summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' + reportDirectory: '$(System.DefaultWorkingDirectory)/htmlcov' + additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/htmlcov/**/*.*' + - task: PublishTestResults@2 condition: succeededOrFailed() displayName: 'Publish Test Results' diff --git a/.azure-devops/templates/set-pythonpath.yml b/.azure-devops/templates/set-pythonpath.yml index c30f4ba05..a9ae4302d 100644 --- a/.azure-devops/templates/set-pythonpath.yml +++ b/.azure-devops/templates/set-pythonpath.yml @@ -1,12 +1,11 @@ steps: - task: PythonScript@0 - displayName : 'Extract extension path' - name: 'extractExtensionPath' + displayName : 'Set Extension Path' + name: 'setExtensionPath' inputs: scriptSource: 'inline' script: | from azure.cli.core.extension import get_extension_path - from six import print_ extension_path = get_extension_path("azure-iot") - print_("Extension path is " + extension_path) - print_("##vso[task.setvariable variable=PYTHONPATH;]"+extension_path) + print("Extension path is " + extension_path) + print("##vso[task.setvariable variable=PYTHONPATH;]"+extension_path) diff --git a/.azure-devops/templates/set-testenv-sentinel.yml b/.azure-devops/templates/set-testenv-sentinel.yml index ef14978cb..94950611e 100644 --- a/.azure-devops/templates/set-testenv-sentinel.yml +++ b/.azure-devops/templates/set-testenv-sentinel.yml @@ -13,11 +13,17 @@ steps: "AZURE_TEST_RUN_LIVE":True, "azext_iot_testrg": os.environ.get("AZEXT_IOT_TESTRG", sentinel_value), "azext_iot_testhub": os.environ.get("AZEXT_IOT_TESTHUB", sentinel_value), - "azext_iot_testhub_cs": os.environ.get("AZEXT_IOT_TESTHUB_CS", 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_testidentity": os.environ.get("AZEXT_IOT_TESTIDENTITY", 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), + "azext_dt_ep_servicebus_policy": os.environ.get("AZEXT_DT_EP_SERVICEBUS_POLICY", sentinel_value), + "azext_dt_ep_servicebus_topic": os.environ.get("AZEXT_DT_EP_SERVICEBUS_TOPIC", sentinel_value), + "azext_dt_ep_eventhub_namespace": os.environ.get("AZEXT_DT_EP_EVENTHUB_NAMESPACE", sentinel_value), + "azext_dt_ep_eventhub_policy": os.environ.get("AZEXT_DT_EP_EVENTHUB_POLICY", sentinel_value), + "azext_dt_ep_eventhub_topic": os.environ.get("AZEXT_DT_EP_EVENTHUB_TOPIC", sentinel_value), + "azext_dt_ep_rg": os.environ.get("AZEXT_DT_EP_RG", sentinel_value), } f = open("./pytest.ini", "w+") f.write("[pytest]\n") @@ -27,12 +33,18 @@ steps: f.writelines(envvars_sentinel) print(envvars_sentinel) f.close() - + env: AZEXT_IOT_TESTRG: $(azext_iot_testrg) AZEXT_IOT_TESTHUB: $(azext_iot_testhub) - AZEXT_IOT_TESTHUB_CS: $(azext_iot_testhub_cs) AZEXT_IOT_TESTDPS: $(azext_iot_testdps) AZEXT_IOT_TESTSTORAGEURI: $(azext_iot_teststorageuri) - AZEXT_IOT_TESTIDENTITY: $(azext_iot_testidentity) - AZEXT_IOT_CENTRAL_APP_ID: $(azext_iot_central_app_id) \ No newline at end of file + 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) + AZEXT_DT_EP_SERVICEBUS_POLICY: $(azext_dt_ep_servicebus_policy) + AZEXT_DT_EP_SERVICEBUS_TOPIC: $(azext_dt_ep_servicebus_topic) + AZEXT_DT_EP_EVENTHUB_NAMESPACE: $(azext_dt_ep_eventhub_namespace) + AZEXT_DT_EP_EVENTHUB_POLICY: $(azext_dt_ep_eventhub_policy) + AZEXT_DT_EP_EVENTHUB_TOPIC: $(azext_dt_ep_eventhub_topic) + AZEXT_DT_EP_RG: $(azext_dt_ep_rg) diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py index 20c9f633f..5b9f13849 100644 --- a/azext_iot/_factory.py +++ b/azext_iot/_factory.py @@ -35,14 +35,9 @@ def iot_hub_service_factory(cli_ctx, *_): working with IoT Hub. """ from azure.cli.core.commands.client_factory import get_mgmt_service_client + from azure.cli.core.profiles import ResourceType - # To support newer and older IotHubClient. 0.9.0+ has breaking changes. - try: - from azure.mgmt.iothub import IotHubClient - except: - # For <0.9.0 - from azure.mgmt.iothub.iot_hub_client import IotHubClient - return get_mgmt_service_client(cli_ctx, IotHubClient).iot_hub_resource + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_IOTHUB).iot_hub_resource def iot_service_provisioning_factory(cli_ctx, *_): diff --git a/azext_iot/azext_metadata.json b/azext_iot/azext_metadata.json index 9d4d4e147..5bed66c74 100644 --- a/azext_iot/azext_metadata.json +++ b/azext_iot/azext_metadata.json @@ -1,3 +1,3 @@ { - "azext.minCliCoreVersion": "2.0.70" -} \ No newline at end of file + "azext.minCliCoreVersion": "2.3.1" +} diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 949e20c36..0d7cf14d6 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.7" +VERSION = "0.9.8" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/digitaltwins/commands_resource.py b/azext_iot/digitaltwins/commands_resource.py index c2454e35a..206e00d91 100644 --- a/azext_iot/digitaltwins/commands_resource.py +++ b/azext_iot/digitaltwins/commands_resource.py @@ -11,7 +11,7 @@ logger = get_logger(__name__) -def create_instance(cmd, name, resource_group_name, location, tags=None): +def create_instance(cmd, name, resource_group_name, location=None, tags=None): rp = ResourceProvider(cmd) return rp.create( name=name, resource_group_name=resource_group_name, location=location, tags=tags diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index 4c25a7c9b..a15ae9336 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -42,7 +42,7 @@ def load_digitaltwins_arguments(self, _): context.argument( "location", options_list=["--location", "-l"], - help="Digital Twins instance location. " + help="Digital Twins instance location. If no location is provided the resource group location is used." "You can configure the default location using `az configure --defaults location=`.", ), context.argument( diff --git a/azext_iot/digitaltwins/providers/resource.py b/azext_iot/digitaltwins/providers/resource.py index de15856c2..842111dc5 100644 --- a/azext_iot/digitaltwins/providers/resource.py +++ b/azext_iot/digitaltwins/providers/resource.py @@ -20,10 +20,15 @@ def __init__(self, cmd): self.mgmt_sdk = self.get_mgmt_sdk() self.rbac = RbacProvider() - def create(self, name, resource_group_name, location, tags=None, timeout=20): + def create(self, name, resource_group_name, location=None, tags=None, timeout=20): if tags: tags = validate_key_value_pairs(tags) + if not location: + from azext_iot.common.embedded_cli import EmbeddedCLI + resource_group_meta = EmbeddedCLI().invoke("group show --name {}".format(resource_group_name)).as_json() + location = resource_group_meta["location"] + try: return self.mgmt_sdk.digital_twins.create_or_update( resource_name=name, diff --git a/azext_iot/tests/__init__.py b/azext_iot/tests/__init__.py index 7580605a6..f7cf926d2 100644 --- a/azext_iot/tests/__init__.py +++ b/azext_iot/tests/__init__.py @@ -69,21 +69,21 @@ def command_execute_assert(self, command, asserts): class IoTLiveScenarioTest(CaptureOutputLiveScenarioTest): - def __init__(self, test_scenario, entity_name, entity_rg, entity_cs): + def __init__(self, test_scenario, entity_name, entity_rg): assert test_scenario assert entity_name assert entity_rg - assert entity_cs self.entity_name = entity_name self.entity_rg = entity_rg - self.entity_cs = entity_cs 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 generate_device_names(self, count=1, edge=False): names = [ @@ -121,7 +121,7 @@ def tearDown(self): device = self.device_ids.pop() self.cmd( "iot hub device-identity delete -d {} --login {}".format( - device, self.entity_cs + device, self.connection_string ), checks=self.is_empty(), ) @@ -138,7 +138,7 @@ def tearDown(self): config = self.config_ids.pop() self.cmd( "iot hub configuration delete -c {} --login {}".format( - config, self.entity_cs + config, self.connection_string ), checks=self.is_empty(), ) @@ -160,6 +160,13 @@ def get_region(self): if loc["role"] == "primary": return loc["location"] + def get_hub_cstring(self, policy="iothubowner"): + return self.cmd( + "iot hub show-connection-string -n {} -g {} --policy-name {}".format( + self.entity_name, self.entity_rg, policy + ) + ).get_output_in_json()["connectionString"] + def disable_telemetry(test_function): def wrapper(*args, **kwargs): diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py index 6a7408a1b..c6b763a18 100644 --- a/azext_iot/tests/digitaltwins/__init__.py +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -7,7 +7,10 @@ import pytest import os from ..generators import generate_generic_id +from ..settings import DynamoSettings from azure.cli.testsdk import LiveScenarioTest +from azext_iot.common.embedded_cli import EmbeddedCLI + MOCK_RESOURCE_TAGS = "a=b;c=d" MOCK_ENDPOINT_TAGS = "key0=value0;key1=value1;" @@ -17,35 +20,26 @@ def generate_resource_id(): return "dt{}".format(generate_generic_id()) -def generate_group_id(): - return "group{}".format(generate_generic_id()) - - class DTLiveScenarioTest(LiveScenarioTest): - group_location = "westus2" - group_names = [] - dt_location_default = "eastus2euap" - role_map = { "owner": "Azure Digital Twins Owner (Preview)", "reader": "Azure Digital Twins Reader (Preview)", } - def __init__(self, test_scenario, group_names): + def __init__(self, test_scenario): assert test_scenario - assert group_names os.environ["AZURE_CORE_COLLECT_TELEMETRY"] = "no" super(DTLiveScenarioTest, self).__init__(test_scenario) - DTLiveScenarioTest.handle = self - - DTLiveScenarioTest.group_names = group_names + self.settings = DynamoSettings( + opt_env_set=["azext_iot_testrg", "azext_dt_resource_location"] + ) + self.embedded_cli = EmbeddedCLI() self._bootup_scenario() def _bootup_scenario(self): self._is_provider_registered() - for group_name in self.group_names: - self.cmd("group create -n {} -l {}".format(group_name, self.group_location)) + self._init_basic_env_vars() def _is_provider_registered(self): result = self.cmd( @@ -53,26 +47,49 @@ def _is_provider_registered(self): ) if '"registered"' in result.output.lower(): return + pytest.skip( "Microsoft.DigitalTwins provider not registered. " "Run 'az provider register --namespace Microsoft.DigitalTwins'" ) + def _init_basic_env_vars(self): + self._location = self.settings.env.azext_dt_resource_location + if not self._location: + self._location = "westus2" + + self._rg = self.settings.env.azext_iot_testrg + if not self._rg: + pytest.skip( + "Digital Twins CLI tests requires at least 'azext_iot_testrg' for resource deployment." + ) + + self._rg_loc = self.embedded_cli.invoke("group show --name {}".format(self._rg)).as_json()["location"] + + @property + def current_user(self): + return self.embedded_cli.invoke("account show").as_json()["user"]["name"] + + @property + def current_subscription(self): + return self.embedded_cli.invoke("account show").as_json()["id"] + @property def dt_location(self): - return self.dt_location_default + return self._location @dt_location.setter def dt_location(self, value): - self.dt_location_default = value - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - target_groups = DTLiveScenarioTest.group_names - # Ensure clean-up after ourselves. - for group in target_groups: - try: - cls.handle.cmd("group delete -n {} -y --no-wait".format(group)) - except: - pass + self._location = value + + @property + def dt_resource_group(self): + return self._rg + + @dt_resource_group.setter + def dt_resource_group(self, value): + self._rg = value + + @property + def dt_resource_group_loc(self): + return self._rg_loc diff --git a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py index 77b5acc42..ee62ac03e 100644 --- a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py @@ -12,55 +12,38 @@ scantree, process_json_arg, ) -from ..settings import DynamoSettings from . import DTLiveScenarioTest from . import ( - generate_resource_id, - generate_group_id, + generate_resource_id ) logger = get_logger(__name__) -model_tests_env_vars = [ - "azext_dt_rbac_assignee_owner", -] -settings = DynamoSettings([], model_tests_env_vars) -run_tests = False - -if all([settings.env.azext_dt_rbac_assignee_owner]): - run_tests = True - - -@pytest.mark.skipif(not run_tests, reason="azext_dt_rbac_assignee_owner is required.") @pytest.mark.usefixtures("set_cwd") class TestDTModelLifecycle(DTLiveScenarioTest): def __init__(self, test_case): - self.group_names = [generate_group_id()] - self.rbac_assignee_owner = settings.env.azext_dt_rbac_assignee_owner - self.dt_location = "westus2" - self.room_dtmi = "dtmi:example:Room;1" - self.floor_dtmi = "dtmi:example:Floor;1" - - super(TestDTModelLifecycle, self).__init__(test_case, self.group_names) + super(TestDTModelLifecycle, self).__init__(test_case) def test_dt_models(self): instance_name = generate_resource_id() models_directory = "./models" inline_model = "./models/Floor.json" - component_id = "dtmi:com:example:Thermostat;1" + component_dtmi = "dtmi:com:example:Thermostat;1" + room_dtmi = "dtmi:example:Room;1" self.cmd( "dt create -n {} -g {} -l {}".format( - instance_name, self.group_names[0], self.dt_location + instance_name, self.dt_resource_group, self.dt_location ) ) self.cmd( "dt role-assignment create -n {} -g {} --assignee {} --role '{}'".format( - instance_name, self.group_names[0], self.rbac_assignee_owner, self.role_map["owner"] + instance_name, self.dt_resource_group, self.current_user, self.role_map["owner"] ) ) + # Wait for RBAC to catch-up sleep(10) @@ -83,7 +66,7 @@ def test_dt_models(self): list_models_output = self.cmd( "dt model list -n {} -g {} --definition".format( - instance_name, self.group_names[0] + instance_name, self.dt_resource_group ) ).get_output_in_json() assert len(list_models_output) == len(create_models_output) @@ -93,7 +76,7 @@ def test_dt_models(self): model_dependencies_output = self.cmd( "dt model list -n {} -g {} --dependencies-for '{}'".format( - instance_name, self.group_names[0], self.room_dtmi, + instance_name, self.dt_resource_group, room_dtmi, ) ).get_output_in_json() assert len(model_dependencies_output) == 2 @@ -106,7 +89,7 @@ def test_dt_models(self): model_show_def_output = self.cmd( "dt model show -n {} -g {} --dtmi '{}' --definition".format( - instance_name, self.group_names[0], model["id"] + instance_name, self.dt_resource_group, model["id"] ) ).get_output_in_json() @@ -138,14 +121,14 @@ def test_dt_models(self): # Delete non-referenced models first for model in list_models_output: - if model["id"] != component_id: + if model["id"] != component_dtmi: self.cmd( "dt model delete -n {} --dtmi {}".format(instance_name, model["id"]) ) # Now referenced component self.cmd( - "dt model delete -n {} --dtmi {}".format(instance_name, component_id) + "dt model delete -n {} --dtmi {}".format(instance_name, component_dtmi) ) assert ( @@ -157,6 +140,10 @@ def test_dt_models(self): == 0 ) + self.cmd( + "dt delete -n {} -g {}".format(instance_name, self.dt_resource_group) + ) + def assert_create_models_attributes( result, directory_path=None, models=None, return_metadata=True 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 f2141a9c3..02569767b 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -15,14 +15,11 @@ MOCK_RESOURCE_TAGS, MOCK_ENDPOINT_TAGS, generate_resource_id, - generate_group_id, ) logger = get_logger(__name__) resource_test_env_vars = [ - "azext_dt_rbac_assignee_owner", - "azext_dt_rbac_assignee_reader", "azext_dt_ep_eventhub_namespace", "azext_dt_ep_eventhub_policy", "azext_dt_ep_eventhub_topic", @@ -30,25 +27,16 @@ "azext_dt_ep_servicebus_policy", "azext_dt_ep_servicebus_topic", "azext_dt_ep_eventgrid_topic", - "azext_dt_ep_rg" + "azext_dt_ep_rg", ] -settings = DynamoSettings([], resource_test_env_vars) -run_rbac_tests = False +settings = DynamoSettings(opt_env_set=resource_test_env_vars) run_resource_tests = False run_endpoint_route_tests = False -if all( - [ - settings.env.azext_dt_rbac_assignee_owner, - settings.env.azext_dt_rbac_assignee_reader, - ] -): - run_rbac_tests = True if all( [ - settings.env.azext_dt_rbac_assignee_owner, settings.env.azext_dt_ep_eventhub_namespace, settings.env.azext_dt_ep_eventhub_policy, settings.env.azext_dt_ep_eventhub_topic, @@ -64,17 +52,16 @@ class TestDTResourceLifecycle(DTLiveScenarioTest): def __init__(self, test_case): - self.group_names = [generate_group_id(), generate_group_id()] - self.instance_names = [generate_resource_id(), generate_resource_id()] - self.dt_location = "westus2" - - super(TestDTResourceLifecycle, self).__init__(test_case, self.group_names) + super(TestDTResourceLifecycle, self).__init__(test_case) def test_dt_resource(self): + instance_names = [generate_resource_id(), generate_resource_id()] + dt_location_custom = "eastus2euap" + create_output = self.cmd( "dt create -n {} -g {} -l {} --tags {}".format( - self.instance_names[0], - self.group_names[0], + instance_names[0], + self.dt_resource_group, self.dt_location, MOCK_RESOURCE_TAGS, ) @@ -82,8 +69,8 @@ def test_dt_resource(self): assert_common_resource_attributes( create_output, - self.instance_names[0], - self.group_names[0], + instance_names[0], + self.dt_resource_group, self.dt_location, MOCK_RESOURCE_TAGS, ) @@ -91,79 +78,77 @@ def test_dt_resource(self): # Explictly assert create prevents provisioning on a name conflict (across regions) self.cmd( "dt create -n {} -g {} -l {} --tags {}".format( - self.instance_names[0], - self.group_names[0], - "eastus2euap", + instance_names[0], + self.dt_resource_group, + dt_location_custom, MOCK_RESOURCE_TAGS, ), expect_failure=True, ) + # No location specified. Use the resource group location. create_output = self.cmd( - "dt create -n {} -g {} -l {}".format( - self.instance_names[1], self.group_names[1], self.dt_location + "dt create -n {} -g {}".format( + instance_names[1], self.dt_resource_group ) ).get_output_in_json() assert_common_resource_attributes( create_output, - self.instance_names[1], - self.group_names[1], - self.dt_location, + instance_names[1], + self.dt_resource_group, + self.dt_resource_group_loc, None, ) show_output = self.cmd( - "dt show -n {}".format(self.instance_names[0],) + "dt show -n {}".format(instance_names[0]) ).get_output_in_json() assert_common_resource_attributes( show_output, - self.instance_names[0], - self.group_names[0], + instance_names[0], + self.dt_resource_group, self.dt_location, MOCK_RESOURCE_TAGS, ) show_output = self.cmd( - "dt show -n {} -g {}".format(self.instance_names[1], self.group_names[1]) + "dt show -n {} -g {}".format(instance_names[1], self.dt_resource_group) ).get_output_in_json() assert_common_resource_attributes( show_output, - self.instance_names[1], - self.group_names[1], + instance_names[1], + self.dt_resource_group, self.dt_location, None, ) list_output = self.cmd("dt list").get_output_in_json() - filtered_list = filter_dt_list(list_output, self.instance_names) - assert len(filtered_list) == len(self.instance_names) + filtered_list = filter_dt_list(list_output, instance_names) + assert len(filtered_list) == len(instance_names) - for group in self.group_names: - list_output = self.cmd("dt list -g {}".format(group)).get_output_in_json() - filtered_group_list = filter_dt_list(list_output, self.instance_names) - assert len(filtered_group_list) == 1 + list_group_output = self.cmd( + "dt list -g {}".format(self.dt_resource_group) + ).get_output_in_json() + filtered_group_list = filter_dt_list(list_group_output, instance_names) + assert len(filtered_group_list) == len(instance_names) # Delete does not currently return output - self.cmd("dt delete -n {}".format(self.instance_names[0])) + self.cmd("dt delete -n {}".format(instance_names[0])) self.cmd( - "dt delete -n {} -g {}".format(self.instance_names[1], self.group_names[1]) + "dt delete -n {} -g {}".format(instance_names[1], self.dt_resource_group) ) - @pytest.mark.skipif( - not run_rbac_tests, - reason="azext_dt_rbac_assignee_owner and azext_dt_rbac_assignee_reader values are required.", - ) def test_dt_rbac(self): - self.rbac_assignee_owner = settings.env.azext_dt_rbac_assignee_owner - self.rbac_assignee_reader = settings.env.azext_dt_rbac_assignee_reader + rbac_assignee_owner = self.current_user + rbac_assignee_reader = self.current_user rbac_instance_name = generate_resource_id() self.cmd( "dt create -n {} -g {} -l {}".format( - rbac_instance_name, self.group_names[0], self.dt_location, + rbac_instance_name, self.dt_resource_group, self.dt_location, ) ) @@ -178,95 +163,86 @@ def test_dt_rbac(self): assign_output = self.cmd( "dt role-assignment create -n {} --assignee {} --role '{}'".format( - rbac_instance_name, self.rbac_assignee_owner, self.role_map["owner"] + rbac_instance_name, rbac_assignee_owner, self.role_map["owner"] ) ).get_output_in_json() assert_common_rbac_attributes( - assign_output, - rbac_instance_name, - "owner", - self.rbac_assignee_owner, + assign_output, rbac_instance_name, "owner", rbac_assignee_owner, ) assign_output = self.cmd( "dt role-assignment create -n {} --assignee {} --role '{}' -g {}".format( rbac_instance_name, - self.rbac_assignee_reader, + rbac_assignee_reader, self.role_map["reader"], - self.group_names[0], + self.dt_resource_group, ) ).get_output_in_json() assert_common_rbac_attributes( - assign_output, - rbac_instance_name, - "reader", - self.rbac_assignee_reader, + assign_output, rbac_instance_name, "reader", rbac_assignee_reader, ) - # Also grant reader to owner principal for assignment removal test later - assign_output = self.cmd( - "dt role-assignment create -n {} --assignee {} --role '{}'".format( - rbac_instance_name, - self.rbac_assignee_owner, - self.role_map["reader"], - ) - ).get_output_in_json() - list_assigned_output = self.cmd( "dt role-assignment list -n {}".format(rbac_instance_name) ).get_output_in_json() - assert len(list_assigned_output) == 3 + assert len(list_assigned_output) == 2 # role-assignment delete does not currently return output - # Remove all role assignments (1) for assignee + # Remove specific role assignment (reader) for assignee self.cmd( - "dt role-assignment delete -n {} --assignee {}".format( - rbac_instance_name, self.rbac_assignee_reader + "dt role-assignment delete -n {} --assignee {} --role '{}'".format( + rbac_instance_name, rbac_assignee_owner, self.role_map["reader"], ) ) - # Remove specific role assignment (reader) for assignee + list_assigned_output = self.cmd( + "dt role-assignment list -n {} -g {}".format( + rbac_instance_name, self.dt_resource_group + ) + ).get_output_in_json() + + assert len(list_assigned_output) == 1 + + # Remove all role assignments for assignee self.cmd( - "dt role-assignment delete -n {} --assignee {} --role '{}'".format( - rbac_instance_name, self.rbac_assignee_owner, self.role_map["reader"], + "dt role-assignment delete -n {} --assignee {}".format( + rbac_instance_name, rbac_assignee_reader ) ) list_assigned_output = self.cmd( "dt role-assignment list -n {} -g {}".format( - rbac_instance_name, self.group_names[0] + rbac_instance_name, self.dt_resource_group ) ).get_output_in_json() - assert len(list_assigned_output) == 1 + assert len(list_assigned_output) == 0 self.cmd("dt delete -n {}".format(rbac_instance_name)) @pytest.mark.skipif( not run_endpoint_route_tests, - reason="azext_dt_rbac_assignee_owner and all azext_dt_ep_* env vars are required.", + reason="All azext_dt_ep_* env vars are required for endpoint and route tests.", ) def test_dt_endpoints_routes(self): endpoints_instance_name = generate_resource_id() self.cmd( "dt create -n {} -g {} -l {}".format( - endpoints_instance_name, self.group_names[0], self.dt_location, + endpoints_instance_name, self.dt_resource_group, self.dt_location, ) ) - self.rbac_assignee_owner = settings.env.azext_dt_rbac_assignee_owner - # Setup RBAC so we can interact with routes self.cmd( "dt role-assignment create -n {} --assignee {} --role '{}' -g {}".format( endpoints_instance_name, - self.rbac_assignee_owner, + self.current_user, self.role_map["owner"], - self.group_names[0], + self.dt_resource_group, ) ) @@ -285,7 +261,7 @@ def test_dt_endpoints_routes(self): add_ep_output = self.cmd( "dt endpoint create eventgrid -n {} -g {} --egg {} --egt {} --en {} --tags {}".format( endpoints_instance_name, - self.group_names[0], + self.dt_resource_group, eventgrid_rg, eventgrid_topic, eventgrid_endpoint, @@ -334,12 +310,13 @@ def test_dt_endpoints_routes(self): logger.debug("Adding eventhub endpoint...") add_ep_output = self.cmd( - "dt endpoint create eventhub -n {} --ehg {} --ehn {} --ehp {} --eh {} --en {}".format( + "dt endpoint create eventhub -n {} --ehg {} --ehn {} --ehp {} --eh {} --ehs {} --en {}".format( endpoints_instance_name, eventhub_rg, eventhub_namespace, eventhub_policy, eventhub_topic, + self.current_subscription, eventhub_endpoint, ) ).get_output_in_json() @@ -360,7 +337,7 @@ def test_dt_endpoints_routes(self): show_ep_output = self.cmd( "dt endpoint show -n {} -g {} --en {}".format( - endpoints_instance_name, self.group_names[0], servicebus_endpoint, + endpoints_instance_name, self.dt_resource_group, servicebus_endpoint, ) ).get_output_in_json() @@ -373,7 +350,7 @@ def test_dt_endpoints_routes(self): list_ep_output = self.cmd( "dt endpoint list -n {} -g {}".format( - endpoints_instance_name, self.group_names[0] + endpoints_instance_name, self.dt_resource_group ) ).get_output_in_json() assert len(list_ep_output) == 3 @@ -397,7 +374,7 @@ def test_dt_endpoints_routes(self): route_name, endpoint_name, filter_value, - "-g {}".format(self.group_names[0]) if is_last else "", + "-g {}".format(self.dt_resource_group) if is_last else "", ) ).get_output_in_json() @@ -409,7 +386,7 @@ def test_dt_endpoints_routes(self): "dt route show -n {} --rn {} {}".format( endpoints_instance_name, route_name, - "-g {}".format(self.group_names[0]) if is_last else "", + "-g {}".format(self.dt_resource_group) if is_last else "", ) ).get_output_in_json() @@ -419,7 +396,7 @@ def test_dt_endpoints_routes(self): list_routes_output = self.cmd( "dt route list -n {} -g {}".format( - endpoints_instance_name, self.group_names[0] + endpoints_instance_name, self.dt_resource_group ) ).get_output_in_json() assert len(list_routes_output) == 3 @@ -431,13 +408,13 @@ def test_dt_endpoints_routes(self): "dt route delete -n {} --rn {} {}".format( endpoints_instance_name, route_name, - "-g {}".format(self.group_names[0]) if is_last else "", + "-g {}".format(self.dt_resource_group) if is_last else "", ) ) list_routes_output = self.cmd( "dt route list -n {} -g {}".format( - endpoints_instance_name, self.group_names[0] + endpoints_instance_name, self.dt_resource_group ) ).get_output_in_json() assert len(list_routes_output) == 0 @@ -452,7 +429,7 @@ def test_dt_endpoints_routes(self): "dt endpoint delete -n {} --en {} {}".format( endpoints_instance_name, endpoint_name, - "-g {}".format(self.group_names[0]) if is_last else "", + "-g {}".format(self.dt_resource_group) if is_last else "", ) ).get_output_in_json() assert delete_ep_output["provisioningState"] == "Deleting" @@ -460,7 +437,9 @@ def test_dt_endpoints_routes(self): sleep(15) # Wait for service to catch-up. Service will fix at some point. self.cmd( - "dt delete -n {} -g {}".format(endpoints_instance_name, self.group_names[0]) + "dt delete -n {} -g {}".format( + endpoints_instance_name, self.dt_resource_group + ) ) 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 2546577cb..329631f91 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -8,56 +8,37 @@ import json from time import sleep from knack.log import get_logger -from ..settings import DynamoSettings from . import DTLiveScenarioTest from . import ( generate_resource_id, - generate_group_id, ) logger = get_logger(__name__) -twin_tests_env_vars = [ - "azext_dt_rbac_assignee_owner", -] -settings = DynamoSettings([], twin_tests_env_vars) -run_tests = False - -if all([settings.env.azext_dt_rbac_assignee_owner]): - run_tests = True - - -@pytest.mark.skipif(not run_tests, reason="azext_dt_rbac_assignee_owner is required.") @pytest.mark.usefixtures("set_cwd") class TestDTTwinLifecycle(DTLiveScenarioTest): def __init__(self, test_case): - self.group_names = [generate_group_id()] - self.instance_names = [generate_resource_id(), generate_resource_id()] - self.rbac_assignee_owner = settings.env.azext_dt_rbac_assignee_owner - self.dt_location = "westus2" - self.instance_name = generate_resource_id() - self.floor_dtmi = "dtmi:example:Floor;1" - self.floor_twin_id = "myfloor" - self.room_dtmi = "dtmi:example:Room;1" - self.room_twin_id = "myroom" - self.thermostat_dtmi = "dtmi:com:example:Thermostat;1" - self.thermostat_component_id = "Thermostat" - - super(TestDTTwinLifecycle, self).__init__(test_case, self.group_names) + super(TestDTTwinLifecycle, self).__init__(test_case) def test_dt_twin(self): + instance_name = generate_resource_id() models_directory = "./models" + floor_dtmi = "dtmi:example:Floor;1" + floor_twin_id = "myfloor" + room_dtmi = "dtmi:example:Room;1" + room_twin_id = "myroom" + thermostat_component_id = "Thermostat" self.cmd( "dt create -n {} -g {} -l {}".format( - self.instance_name, self.group_names[0], self.dt_location + instance_name, self.dt_resource_group, self.dt_location ) ) self.cmd( "dt role-assignment create -n {} -g {} --assignee {} --role '{}'".format( - self.instance_name, self.group_names[0], self.rbac_assignee_owner, self.role_map["owner"] + instance_name, self.dt_resource_group, self.current_user, self.role_map["owner"] ) ) # Wait for RBAC to catch-up @@ -65,13 +46,13 @@ def test_dt_twin(self): self.cmd( "dt model create -n {} --from-directory '{}'".format( - self.instance_name, models_directory + instance_name, models_directory ) ) twin_query_result = self.cmd( "dt twin query -n {} -q 'select * from digitaltwins'".format( - self.instance_name + instance_name ) ).get_output_in_json() assert len(twin_query_result["result"]) == 0 @@ -88,42 +69,42 @@ def test_dt_twin(self): floor_twin = self.cmd( "dt twin create -n {} --dtmi {} --twin-id {}".format( - self.instance_name, self.floor_dtmi, self.floor_twin_id + instance_name, floor_dtmi, floor_twin_id ) ).get_output_in_json() assert_twin_attributes( twin=floor_twin, - expected_twin_id=self.floor_twin_id, - expected_dtmi=self.floor_dtmi, + expected_twin_id=floor_twin_id, + expected_dtmi=floor_dtmi, ) room_twin = self.cmd( "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( - self.instance_name, - self.group_names[0], - self.room_dtmi, - self.room_twin_id, + instance_name, + self.dt_resource_group, + room_dtmi, + room_twin_id, "{tempAndThermostatComponentJson}", ) ).get_output_in_json() assert_twin_attributes( twin=room_twin, - expected_twin_id=self.room_twin_id, - expected_dtmi=self.room_dtmi, + expected_twin_id=room_twin_id, + expected_dtmi=room_dtmi, properties=self.kwargs["tempAndThermostatComponentJson"], - component_name=self.thermostat_component_id, + component_name=thermostat_component_id, ) # Component thermostat_component = self.cmd( "dt twin component show -n {} -g {} --twin-id {} --component {}".format( - self.instance_name, - self.group_names[0], - self.room_twin_id, - self.thermostat_component_id, + instance_name, + self.dt_resource_group, + room_twin_id, + thermostat_component_id, ) ).get_output_in_json() @@ -134,20 +115,20 @@ def test_dt_twin(self): # Currently component update does not return value self.cmd( "dt twin component update -n {} -g {} --twin-id {} --component {} --json-patch '{}'".format( - self.instance_name, - self.group_names[0], - self.room_twin_id, - self.thermostat_component_id, + instance_name, + self.dt_resource_group, + room_twin_id, + thermostat_component_id, "{thermostatJsonPatch}", ) ) thermostat_component = self.cmd( "dt twin component show -n {} -g {} --twin-id {} --component {}".format( - self.instance_name, - self.group_names[0], - self.room_twin_id, - self.thermostat_component_id, + instance_name, + self.dt_resource_group, + room_twin_id, + thermostat_component_id, ) ).get_output_in_json() @@ -157,16 +138,16 @@ def test_dt_twin(self): ) twins_id_list = [ - (self.floor_twin_id, self.floor_dtmi), - (self.room_twin_id, self.room_dtmi), + (floor_twin_id, floor_dtmi), + (room_twin_id, room_dtmi), ] for twin_tuple in twins_id_list: twin = self.cmd( "dt twin show -n {} --twin-id {} {}".format( - self.instance_name, + instance_name, twin_tuple[0], - "-g {}".format(self.group_names[0]) + "-g {}".format(self.dt_resource_group) if twins_id_list[-1] == twin_tuple else "", ) @@ -181,7 +162,7 @@ def test_dt_twin(self): update_twin_result = self.cmd( "dt twin update -n {} --twin-id {} --json-patch '{}'".format( - self.instance_name, self.room_twin_id, "{temperatureJsonPatch}", + instance_name, room_twin_id, "{temperatureJsonPatch}", ) ).get_output_in_json() @@ -192,7 +173,7 @@ def test_dt_twin(self): twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins'".format( - self.instance_name, self.group_names[0] + instance_name, self.dt_resource_group ) ).get_output_in_json() assert len(twin_query_result["result"]) == 2 @@ -209,12 +190,12 @@ def test_dt_twin(self): twin_relationship_create_result = self.cmd( "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --twin-id {} " "--target-twin-id {} --properties '{}'".format( - self.instance_name, - self.group_names[0], + instance_name, + self.dt_resource_group, relationship_id, relationship, - self.floor_twin_id, - self.room_twin_id, + floor_twin_id, + room_twin_id, "{relationshipJson}", ) ).get_output_in_json() @@ -223,16 +204,16 @@ def test_dt_twin(self): twin_relationship_obj=twin_relationship_create_result, expected_relationship=relationship, relationship_id=relationship_id, - source_id=self.floor_twin_id, - target_id=self.room_twin_id, + source_id=floor_twin_id, + target_id=room_twin_id, properties=self.kwargs["relationshipJson"], ) twin_relationship_show_result = self.cmd( "dt twin relationship show -n {} -g {} --twin-id {} --relationship-id {}".format( - self.instance_name, - self.group_names[0], - self.floor_twin_id, + instance_name, + self.dt_resource_group, + floor_twin_id, relationship_id, ) ).get_output_in_json() @@ -241,18 +222,18 @@ def test_dt_twin(self): twin_relationship_obj=twin_relationship_show_result, expected_relationship=relationship, relationship_id=relationship_id, - source_id=self.floor_twin_id, - target_id=self.room_twin_id, + source_id=floor_twin_id, + target_id=room_twin_id, properties=self.kwargs["relationshipJson"], ) twin_edge_update_result = self.cmd( "dt twin relationship update -n {} -g {} --relationship-id {} --twin-id {} " "--json-patch '{}'".format( - self.instance_name, - self.group_names[0], + instance_name, + self.dt_resource_group, relationship_id, - self.floor_twin_id, + floor_twin_id, "{relationshipJsonPatch}", ) ).get_output_in_json() @@ -264,16 +245,16 @@ def test_dt_twin(self): twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} --twin-id {}".format( - self.instance_name, self.floor_twin_id, + instance_name, floor_twin_id, ) ).get_output_in_json() assert len(twin_relationship_list_result) == 1 twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} -g {} --twin-id {} --relationship {}".format( - self.instance_name, - self.group_names[0], - self.floor_twin_id, + instance_name, + self.dt_resource_group, + floor_twin_id, relationship, ) ).get_output_in_json() @@ -281,21 +262,21 @@ def test_dt_twin(self): twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} --twin-id {}".format( - self.instance_name, self.room_twin_id, + instance_name, room_twin_id, ) ).get_output_in_json() assert len(twin_relationship_list_result) == 0 twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} --twin-id {} --incoming".format( - self.instance_name, self.room_twin_id, + instance_name, room_twin_id, ) ).get_output_in_json() assert len(twin_relationship_list_result) == 1 twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} --twin-id {} --kind {} --incoming".format( - self.instance_name, self.room_twin_id, relationship + instance_name, room_twin_id, relationship ) ).get_output_in_json() assert len(twin_relationship_list_result) == 1 @@ -303,15 +284,15 @@ def test_dt_twin(self): # No output from API for delete edge self.cmd( "dt twin relationship delete -n {} --twin-id {} -r {}".format( - self.instance_name, self.floor_twin_id, relationship_id, + instance_name, floor_twin_id, relationship_id, ) ) twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} -g {} --twin-id {} --kind {}".format( - self.instance_name, - self.group_names[0], - self.floor_twin_id, + instance_name, + self.dt_resource_group, + floor_twin_id, relationship, ) ).get_output_in_json() @@ -323,19 +304,19 @@ def test_dt_twin(self): self.cmd( "dt twin telemetry send -n {} -g {} --twin-id {} --telemetry '{}'".format( - self.instance_name, - self.group_names[0], - self.room_twin_id, + instance_name, + self.dt_resource_group, + room_twin_id, "{telemetryJson}", ) ) self.cmd( "dt twin telemetry send -n {} -g {} --twin-id {} --component {} --telemetry '{}'".format( - self.instance_name, - self.group_names[0], - self.room_twin_id, - self.thermostat_component_id, + instance_name, + self.dt_resource_group, + room_twin_id, + thermostat_component_id, "{telemetryJson}", ) ) @@ -344,9 +325,9 @@ def test_dt_twin(self): # No output from API for delete twin self.cmd( "dt twin delete -n {} --twin-id {} {}".format( - self.instance_name, + instance_name, twin_tuple[0], - "-g {}".format(self.group_names[0]) + "-g {}".format(self.dt_resource_group) if twins_id_list[-1] == twin_tuple else "", ) @@ -354,12 +335,16 @@ def test_dt_twin(self): sleep(4) # Wait for API to catch up twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins' --cost".format( - self.instance_name, self.group_names[0] + instance_name, self.dt_resource_group ) ).get_output_in_json() assert len(twin_query_result["result"]) == 0 assert twin_query_result["cost"] + self.cmd( + "dt delete -n {} -g {}".format(instance_name, self.dt_resource_group) + ) + # TODO: Refactor - limited interface def assert_twin_attributes( 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 7085c0300..65b18f1c5 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_int.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_int.py @@ -15,7 +15,6 @@ settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) LIVE_HUB = settings.env.azext_iot_testhub LIVE_RG = settings.env.azext_iot_testrg -LIVE_HUB_CS = settings.env.azext_iot_testhub_cs edge_content_path = get_context_path(__file__, "test_edge_deployment.json") edge_content_layered_path = get_context_path( @@ -33,7 +32,7 @@ class TestIoTEdgeSetModules(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTEdgeSetModules, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS + test_case, LIVE_HUB, LIVE_RG ) def test_edge_set_modules(self): @@ -67,7 +66,7 @@ def test_edge_set_modules(self): # Using connection string - content from file self.cmd( "iot edge set-modules -d {} --login {} -k '{}'".format( - edge_device_ids[0], LIVE_HUB_CS, edge_content_v1_path + edge_device_ids[0], self.connection_string, edge_content_v1_path ), checks=[self.check("length([*])", 4)], ) @@ -84,7 +83,7 @@ def test_edge_set_modules(self): class TestIoTEdgeDeployments(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTEdgeDeployments, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS + test_case, LIVE_HUB, LIVE_RG ) def test_edge_deployments(self): @@ -138,7 +137,7 @@ def test_edge_deployments(self): self.cmd( "iot edge deployment create -d {} --login {} --pri {} --tc \"{}\" --lab '{}' -k '{}' --metrics '{}'".format( config_ids[1].upper(), - LIVE_HUB_CS, + self.connection_string, priority, condition, "{labels}", @@ -168,7 +167,7 @@ def test_edge_deployments(self): self.cmd( "iot edge deployment create -d {} --login {} -k '{}' --metrics '{}' --layered".format( config_ids[2].upper(), - LIVE_HUB_CS, + self.connection_string, edge_content_layered_path, generic_metrics_path, ), @@ -267,7 +266,7 @@ def test_edge_deployments(self): # Show deployment - using connection string self.cmd( "iot edge deployment show -d {} --login {}".format( - config_ids[1], LIVE_HUB_CS + config_ids[1], self.connection_string ), checks=[ self.check("id", config_ids[1]), @@ -324,7 +323,7 @@ def test_edge_deployments(self): system_metric_name = "appliedCount" config_output = self.cmd( "iot edge deployment show --login {} --deployment-id {}".format( - LIVE_HUB_CS, config_ids[1] + self.connection_string, config_ids[1] ) ).get_output_in_json() @@ -344,7 +343,7 @@ def test_edge_deployments(self): # System metric - using connection string self.cmd( "iot edge deployment show-metric --metric-id {} --login '{}' --deployment-id {} --metric-type {}".format( - system_metric_name, LIVE_HUB_CS, config_ids[1], "system" + system_metric_name, self.connection_string, config_ids[1], "system" ), checks=[ self.check("metric", system_metric_name), @@ -358,7 +357,7 @@ def test_edge_deployments(self): # Error - metric does not exist, using connection string self.cmd( "iot edge deployment show-metric -m {} --login {} -d {}".format( - "doesnotexist", LIVE_HUB_CS, config_ids[0] + "doesnotexist", self.connection_string, config_ids[0] ), expect_failure=True, ) @@ -379,7 +378,7 @@ def test_edge_deployments(self): # List all edge deployments - using connection string self.cmd( - "iot edge deployment list --login {}".format(LIVE_HUB_CS), + "iot edge deployment list --login {}".format(self.connection_string), checks=config_list_check, ) @@ -406,7 +405,7 @@ def test_edge_deployments(self): # Explicitly delete an edge deployment - using connection string self.cmd( "iot edge deployment delete -d {} --login {}".format( - config_ids[1], LIVE_HUB_CS + config_ids[1], self.connection_string ) ) del self.config_ids[0] @@ -415,7 +414,7 @@ def test_edge_deployments(self): class TestIoTHubConfigurations(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTHubConfigurations, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS + test_case, LIVE_HUB, LIVE_RG ) def test_device_configurations(self): @@ -466,7 +465,7 @@ def test_device_configurations(self): self.cmd( "iot hub configuration create -c {} --login {} --pri {} --tc \"{}\" --lab '{}' -k '{}' --metrics '{}'".format( config_ids[1].upper(), - LIVE_HUB_CS, + self.connection_string, priority, module_condition, "{labels}", @@ -496,7 +495,7 @@ def test_device_configurations(self): self.cmd( "iot hub configuration create -c {} --login {} -k '{}' --metrics '{}'".format( config_ids[2].upper(), - LIVE_HUB_CS, + self.connection_string, adm_content_device_path, generic_metrics_path, ), @@ -539,7 +538,7 @@ def test_device_configurations(self): self.cmd( "iot hub configuration create -c {} --login {} -k '{}'".format( config_ids[1].upper(), - LIVE_HUB_CS, + self.connection_string, adm_content_module_path, ), expect_failure=True, @@ -561,7 +560,7 @@ def test_device_configurations(self): # Show ADM configuration - using connection string self.cmd( "iot hub configuration show -c {} --login {}".format( - config_ids[1], LIVE_HUB_CS + config_ids[1], self.connection_string ), checks=[ self.check("id", config_ids[1]), @@ -618,7 +617,7 @@ def test_device_configurations(self): system_metric_name = "appliedCount" config_output = self.cmd( "iot hub configuration show --login {} --config-id {}".format( - LIVE_HUB_CS, config_ids[1] + self.connection_string, config_ids[1] ) ).get_output_in_json() @@ -638,7 +637,7 @@ def test_device_configurations(self): # System metric - using connection string self.cmd( "iot hub configuration show-metric --metric-id {} --login '{}' --config-id {} --metric-type {}".format( - system_metric_name, LIVE_HUB_CS, config_ids[1], "system" + system_metric_name, self.connection_string, config_ids[1], "system" ), checks=[ self.check("metric", system_metric_name), @@ -652,7 +651,7 @@ def test_device_configurations(self): # Error - metric does not exist, using connection string self.cmd( "iot hub configuration show-metric -m {} --login {} -c {}".format( - "doesnotexist", LIVE_HUB_CS, config_ids[0] + "doesnotexist", self.connection_string, config_ids[0] ), expect_failure=True, ) @@ -682,7 +681,7 @@ def test_device_configurations(self): # List all ADM configurations - using connection string self.cmd( - "iot hub configuration list --login {}".format(LIVE_HUB_CS), + "iot hub configuration list --login {}".format(self.connection_string), checks=config_list_check, ) @@ -709,7 +708,7 @@ def test_device_configurations(self): # Explicitly delete an ADM configuration - using connection string self.cmd( "iot hub configuration delete -c {} --login {}".format( - config_ids[1], LIVE_HUB_CS + config_ids[1], self.connection_string ) ) del self.config_ids[0] diff --git a/azext_iot/tests/iothub/device_identity/__init__.py b/azext_iot/tests/iothub/device_identity/__init__.py deleted file mode 100644 index 55614acbf..000000000 --- a/azext_iot/tests/iothub/device_identity/__init__.py +++ /dev/null @@ -1,5 +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. -# -------------------------------------------------------------------------------------------- diff --git a/azext_iot/tests/iothub/device_identity/test_iot_device_identity_int.py b/azext_iot/tests/iothub/device_identity/test_iot_device_identity_int.py deleted file mode 100644 index de33127d2..000000000 --- a/azext_iot/tests/iothub/device_identity/test_iot_device_identity_int.py +++ /dev/null @@ -1,22 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from ... import 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 -LIVE_HUB_CS = settings.env.azext_iot_testhub_cs - - -class TestIoTDeviceIdentity(IoTLiveScenarioTest): - def __init__(self, test_case): - super(TestIoTDeviceIdentity, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS - ) - - pass 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 1223cb87e..d9c47df0b 100644 --- a/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py +++ b/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py @@ -13,12 +13,11 @@ settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) LIVE_HUB = settings.env.azext_iot_testhub LIVE_RG = settings.env.azext_iot_testrg -LIVE_HUB_CS = settings.env.azext_iot_testhub_cs class TestIoTHubJobs(IoTLiveScenarioTest): def __init__(self, test_case): - super(TestIoTHubJobs, self).__init__(test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS) + super(TestIoTHubJobs, self).__init__(test_case, LIVE_HUB, LIVE_RG) job_count = 3 self.job_ids = self.generate_job_names(job_count) @@ -89,7 +88,7 @@ def test_jobs(self): "scheduleUpdateTwin", query_condition, "{twin_patch_props}", - LIVE_HUB_CS, + self.connection_string, ), checks=[ self.check("jobId", self.job_ids[1]), @@ -150,7 +149,7 @@ def test_jobs(self): # With connection string self.cmd( "iot hub job show --job-id {} --login {}".format( - self.job_ids[1], LIVE_HUB_CS + self.job_ids[1], self.connection_string ), checks=[ self.check("jobId", self.job_ids[1]), @@ -211,7 +210,7 @@ def test_jobs(self): # List Jobs - with connection string job_result_set_cs = self.cmd( - "iot hub job list --login {}".format(LIVE_HUB_CS) + "iot hub job list --login {}".format(self.connection_string) ).get_output_in_json() self.validate_job_list(jobs_set=job_result_set_cs) diff --git a/azext_iot/tests/settings.py b/azext_iot/tests/settings.py index de665c825..6c865c170 100644 --- a/azext_iot/tests/settings.py +++ b/azext_iot/tests/settings.py @@ -10,7 +10,6 @@ ENV_SET_TEST_IOTHUB_BASIC = [ "azext_iot_testhub", "azext_iot_testrg", - "azext_iot_testhub_cs", ] @@ -21,7 +20,10 @@ class Setting(object): # Example of a dynamic class # TODO: Evaluate moving this to the extension prime time class DynamoSettings(object): - def __init__(self, req_env_set, opt_env_set=None): + def __init__(self, req_env_set: list = None, opt_env_set: list = None): + if not req_env_set: + req_env_set = [] + if not isinstance(req_env_set, list): raise TypeError("req_env_set must be a list") @@ -33,12 +35,12 @@ def __init__(self, req_env_set, opt_env_set=None): raise TypeError("opt_env_set must be a list") self._build_config(opt_env_set, optional=True) - def _build_config(self, env_set, optional=False): + def _build_config(self, env_set: list, optional: bool = False): for key in env_set: value = environ.get(key) if not value: if not optional: raise RuntimeError( - "'{}' environment variable required.".format(key) + "{} environment variables required.".format(",".join(env_set)) ) setattr(self.env, key, value) diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index e3914ffe0..5ce4a1743 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -21,8 +21,6 @@ LIVE_HUB = settings.env.azext_iot_testhub LIVE_RG = settings.env.azext_iot_testrg -LIVE_HUB_CS = settings.env.azext_iot_testhub_cs -LIVE_HUB_MIXED_CASE_CS = LIVE_HUB_CS.replace("HostName", "hostname", 1) # 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. @@ -42,7 +40,7 @@ class TestIoTHub(IoTLiveScenarioTest): def __init__(self, test_case): - super(TestIoTHub, self).__init__(test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS) + super(TestIoTHub, self).__init__(test_case, LIVE_HUB, LIVE_RG) def test_hub(self): self.cmd( @@ -62,13 +60,13 @@ def test_hub(self): # With connection string self.cmd( - "az iot hub generate-sas-token --login {}".format(LIVE_HUB_CS), + "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( - LIVE_HUB_CS + self.connection_string ), expect_failure=True, ) @@ -77,7 +75,7 @@ def test_hub(self): # Error can't change key for a sas token with conn string self.cmd( "az iot hub generate-sas-token --login {} --kt secondary".format( - LIVE_HUB_CS + self.connection_string ), expect_failure=True, ) @@ -92,7 +90,7 @@ def test_hub(self): # With connection string self.cmd( 'iot hub query --query-command "{}" --login {}'.format( - "select * from devices", LIVE_HUB_CS + "select * from devices", self.connection_string ), checks=[self.check("length([*])", 0)], ) @@ -113,7 +111,7 @@ def test_hub(self): class TestIoTHubDevices(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTHubDevices, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS + test_case, LIVE_HUB, LIVE_RG ) def test_hub_devices(self): @@ -200,7 +198,7 @@ def test_hub_devices(self): # With connection string self.cmd( 'iot hub query -q "{}" --login {}'.format( - "select * from devices", LIVE_HUB_CS + "select * from devices", self.connection_string ), checks=query_checks, ) @@ -208,7 +206,7 @@ def test_hub_devices(self): # -1 for no return limit self.cmd( 'iot hub query -q "{}" --login {} --top -1'.format( - "select * from devices", LIVE_HUB_CS + "select * from devices", self.connection_string ), checks=query_checks, ) @@ -264,7 +262,7 @@ def test_hub_devices(self): self.cmd( '''iot hub device-identity create --device-id {} --login {} --auth-method x509_ca --status disabled --status-reason "{}"'''.format( - device_ids[2], LIVE_HUB_CS, status_reason + device_ids[2], self.connection_string, status_reason ), checks=[ self.check("deviceId", device_ids[2]), @@ -322,7 +320,7 @@ def test_hub_devices(self): # With connection string self.cmd( "iot hub device-identity show -d {} --login {}".format( - edge_device_ids[0], LIVE_HUB_CS + edge_device_ids[0], self.connection_string ), checks=[ self.check("deviceId", edge_device_ids[0]), @@ -358,7 +356,7 @@ def test_hub_devices(self): # With connection string self.cmd( - "iot hub device-identity list --ee --login {}".format(LIVE_HUB_CS), + "iot hub device-identity list --ee --login {}".format(self.connection_string), checks=[self.check("length([*])", edge_device_count)], ) @@ -400,7 +398,7 @@ def test_hub_devices(self): self.cmd( '''iot hub device-identity update -d {} --login {} --set authentication.symmetricKey.primaryKey="" authentication.symmetricKey.secondaryKey=""'''.format( - edge_device_ids[1], LIVE_HUB_CS + edge_device_ids[1], self.connection_string ), checks=[ self.check("deviceId", edge_device_ids[1]), @@ -470,21 +468,21 @@ def test_hub_devices(self): # With connection string self.cmd( "iot hub generate-sas-token -d {} --login {}".format( - edge_device_ids[0], LIVE_HUB_CS + 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], LIVE_HUB_CS + 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], LIVE_HUB_CS + edge_device_ids[1], self.connection_string ), expect_failure=True, ) @@ -493,7 +491,7 @@ def test_hub_devices(self): class TestIoTHubDeviceTwins(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTHubDeviceTwins, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS + test_case, LIVE_HUB, LIVE_RG ) def test_hub_device_twins(self): @@ -528,7 +526,7 @@ def test_hub_device_twins(self): # With connection string self.cmd( "iot hub device-twin show -d {} --login {}".format( - device_ids[0], LIVE_HUB_CS + device_ids[0], self.connection_string ), checks=[ self.check("deviceId", device_ids[0]), @@ -558,7 +556,7 @@ def test_hub_device_twins(self): # Patch based twin update of tags with connection string self.cmd( "iot hub device-twin update -d {} --login {} --tags {}".format( - device_ids[2], LIVE_HUB_CS, '"{patch_tags}"' + device_ids[2], self.connection_string, '"{patch_tags}"' ), checks=[ self.check("deviceId", device_ids[2]), @@ -610,7 +608,7 @@ def test_hub_device_twins(self): # With connection string result = self.cmd( "iot hub device-twin update -d {} --login {} --set properties.desired.special={}".format( - device_ids[0], LIVE_HUB_CS, '"{generic_dict}"' + device_ids[0], self.connection_string, '"{generic_dict}"' ) ).get_output_in_json() assert result["deviceId"] == device_ids[0] @@ -655,7 +653,7 @@ def test_hub_device_twins(self): # With connection string self.cmd( "iot hub device-twin replace -d {} --login {} -j '{}'".format( - device_ids[1], LIVE_HUB_CS, "{twin_payload}" + device_ids[1], self.connection_string, "{twin_payload}" ), checks=[ self.check("deviceId", device_ids[1]), @@ -693,7 +691,7 @@ def test_hub_device_twins(self): class TestIoTHubModules(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTHubModules, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS + test_case, LIVE_HUB, LIVE_RG ) def test_hub_modules(self): @@ -736,7 +734,7 @@ def test_hub_modules(self): self.cmd( "iot hub module-identity create -d {} --login {} -m {}".format( - edge_device_ids[0], LIVE_HUB_CS, module_ids[0] + edge_device_ids[0], self.connection_string, module_ids[0] ), checks=[ self.check("deviceId", edge_device_ids[0]), @@ -765,15 +763,16 @@ def test_hub_modules(self): # 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], LIVE_HUB_CS + 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], LIVE_HUB_MIXED_CASE_CS + edge_device_ids[0], module_ids[1], mixed_case_cstring ), checks=[self.exists("sas")], ) @@ -785,7 +784,7 @@ def test_hub_modules(self): --auth-method x509_thumbprint --primary-thumbprint {} --secondary-thumbprint {}""".format( module_ids[0], device_ids[0], - LIVE_HUB_CS, + self.connection_string, PRIMARY_THUMBPRINT, SECONDARY_THUMBPRINT, ), @@ -825,7 +824,7 @@ def test_hub_modules(self): # 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], LIVE_HUB_CS + module_ids[0], edge_device_ids[1], self.connection_string ), checks=[ self.check("deviceId", edge_device_ids[1]), @@ -867,7 +866,7 @@ def test_hub_modules(self): self.cmd( '''iot hub module-identity update -d {} --login {} -m {} --set authentication.symmetricKey.primaryKey="" authentication.symmetricKey.secondaryKey=""'''.format( - edge_device_ids[0], LIVE_HUB_CS, module_ids[0] + edge_device_ids[0], self.connection_string, module_ids[0] ), checks=[ self.check("deviceId", edge_device_ids[0]), @@ -902,7 +901,7 @@ def test_hub_modules(self): # With connection string self.cmd( "iot hub module-identity list -d {} --login {}".format( - edge_device_ids[0], LIVE_HUB_CS + edge_device_ids[0], self.connection_string ), checks=[ self.check("length([*])", 4), @@ -926,7 +925,7 @@ def test_hub_modules(self): # With connection string self.cmd( "iot hub module-identity show -d {} --login {} -m {}".format( - edge_device_ids[0], LIVE_HUB_CS, module_ids[0] + edge_device_ids[0], self.connection_string, module_ids[0] ), checks=[ self.check("deviceId", edge_device_ids[0]), @@ -949,7 +948,7 @@ def test_hub_modules(self): # With connection string self.cmd( "iot hub module-identity show-connection-string -d {} --login {} -m {}".format( - edge_device_ids[0], LIVE_HUB_CS, module_ids[0] + edge_device_ids[0], self.connection_string, module_ids[0] ), checks=[self.check_pattern("connectionString", mod_sym_conn_str_pattern)], ) @@ -966,7 +965,7 @@ def test_hub_modules(self): # With connection string self.cmd( "iot hub module-identity delete -d {} --login {} --module-id {}".format( - edge_device_ids[0], LIVE_HUB_CS, i + edge_device_ids[0], self.connection_string, i ), checks=self.is_empty(), ) @@ -982,7 +981,7 @@ def test_hub_modules(self): class TestIoTHubModuleTwins(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTHubModuleTwins, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS + test_case, LIVE_HUB, LIVE_RG ) def test_hub_module_twins(self): @@ -1052,7 +1051,7 @@ def test_hub_module_twins(self): # With connection string self.cmd( "iot hub module-twin show -d {} --login {} -m {}".format( - edge_device_ids[0], LIVE_HUB_CS, module_ids[0] + edge_device_ids[0], self.connection_string, module_ids[0] ), checks=[ self.check("deviceId", edge_device_ids[0]), @@ -1084,7 +1083,7 @@ def test_hub_module_twins(self): # 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], LIVE_HUB_CS, module_ids[0], '"{patch_tags}"' + edge_device_ids[0], self.connection_string, module_ids[0], '"{patch_tags}"' ), checks=[ self.check("deviceId", edge_device_ids[0]), @@ -1133,7 +1132,7 @@ def test_hub_module_twins(self): # With connection string self.cmd( "iot hub module-twin update -d {} --login {} -m {} --set properties.desired.special={}".format( - edge_device_ids[0], LIVE_HUB_CS, module_ids[0], '"{generic_dict}"' + edge_device_ids[0], self.connection_string, module_ids[0], '"{generic_dict}"' ), checks=[ self.check("deviceId", edge_device_ids[0]), @@ -1145,14 +1144,14 @@ def test_hub_module_twins(self): # Error case test type enforcer self.cmd( "iot hub module-twin update -d {} --login {} -m {} --set properties.desired={}".format( - edge_device_ids[0], LIVE_HUB_CS, module_ids[0], '"{bad_format}"' + edge_device_ids[0], self.connection_string, module_ids[0], '"{bad_format}"' ), expect_failure=True, ) self.cmd( "iot hub module-twin update -d {} --login {} -m {} --set tags={}".format( - edge_device_ids[0], LIVE_HUB_CS, module_ids[0], '"{bad_format}"' + edge_device_ids[0], self.connection_string, module_ids[0], '"{bad_format}"' ), expect_failure=True, ) @@ -1175,7 +1174,7 @@ def test_hub_module_twins(self): # With connection string self.cmd( "iot hub module-twin replace -d {} --login {} -m {} -j '{}'".format( - edge_device_ids[0], LIVE_HUB_CS, module_ids[0], content_path + edge_device_ids[0], self.connection_string, module_ids[0], content_path ), checks=[ self.check("deviceId", edge_device_ids[0]), @@ -1213,7 +1212,7 @@ def test_hub_module_twins(self): class TestIoTStorage(IoTLiveScenarioTest): def __init__(self, test_case): - super(TestIoTStorage, self).__init__(test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS) + super(TestIoTStorage, self).__init__(test_case, LIVE_HUB, LIVE_RG) @pytest.mark.skipif( not LIVE_STORAGE, reason="empty azext_iot_teststorageuri env var" @@ -1241,7 +1240,7 @@ def test_storage(self): # With connection string self.cmd( 'iot device upload-file -d {} --login {} --fp "{}" --ct {}'.format( - device_ids[0], LIVE_HUB_CS, content_path, "application/json" + device_ids[0], self.connection_string, content_path, "application/json" ), checks=self.is_empty(), ) @@ -1279,7 +1278,7 @@ def test_identity_storage(self): class TestIoTEdgeOffline(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTEdgeOffline, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS + test_case, LIVE_HUB, LIVE_RG ) def test_edge_offline(self): diff --git a/azext_iot/tests/test_iot_messaging_int.py b/azext_iot/tests/test_iot_messaging_int.py index 99af5cffa..ae5e9743e 100644 --- a/azext_iot/tests/test_iot_messaging_int.py +++ b/azext_iot/tests/test_iot_messaging_int.py @@ -4,15 +4,13 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import os import pytest import json from time import time from uuid import uuid4 from . import IoTLiveScenarioTest, PREFIX_DEVICE - -# Temporary workaround. +from .settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC from azext_iot.common.utility import ( validate_min_python_version, execute_onthread, @@ -20,25 +18,17 @@ validate_key_value_pairs ) -# Set these to the proper IoT Hub, IoT Hub Cstring and Resource Group for Live Integration Tests. -LIVE_HUB = os.environ.get("azext_iot_testhub") -LIVE_RG = os.environ.get("azext_iot_testrg") -LIVE_HUB_CS = os.environ.get("azext_iot_testhub_cs") +settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg LIVE_CONSUMER_GROUPS = ["test1", "test2", "test3"] -if not all([LIVE_HUB, LIVE_HUB_CS, LIVE_RG]): - raise ValueError( - "Set azext_iot_testhub, azext_iot_testhub_cs and azext_iot_testrg to run IoT Hub integration tests." - ) - - -# IoT Hub Messaging tests currently are run live due to non HTTP based interaction i.e. amqp, mqtt. class TestIoTHubMessaging(IoTLiveScenarioTest): def __init__(self, test_case): super(TestIoTHubMessaging, self).__init__( - test_case, LIVE_HUB, LIVE_RG, LIVE_HUB_CS + test_case, LIVE_HUB, LIVE_RG ) @pytest.mark.skipif( @@ -127,7 +117,7 @@ def test_uamqp_device_messaging(self): """iot device c2d-message send -d {} --login {} --data '{}' --cid {} --mid {} --ct {} --expiry {} --ce {} --ack positive --props {}""".format( device_ids[0], - LIVE_HUB_CS, + self.connection_string, "{c2d_json_send_data}", test_cid, test_mid, @@ -141,7 +131,7 @@ def test_uamqp_device_messaging(self): result = self.cmd( "iot device c2d-message receive -d {} --login {}".format( - device_ids[0], LIVE_HUB_CS + device_ids[0], self.connection_string ) ).get_output_in_json() @@ -166,7 +156,7 @@ def test_uamqp_device_messaging(self): self.cmd( "iot device c2d-message reject -d {} --etag {} --login {}".format( - device_ids[0], etag, LIVE_HUB_CS + device_ids[0], etag, self.connection_string ), checks=self.is_empty(), ) @@ -197,7 +187,7 @@ def test_uamqp_device_messaging(self): self.cmd( "iot device c2d-message send -d {} --ack {} --login {} --wait -y".format( - device_ids[0], "full", LIVE_HUB_CS + device_ids[0], "full", self.connection_string ) ) token.set() @@ -206,7 +196,7 @@ def test_uamqp_device_messaging(self): # Error - invalid wait when no ack requested self.cmd( "iot device c2d-message send -d {} --login {} --wait -y".format( - device_ids[0], LIVE_HUB_CS + device_ids[0], self.connection_string ), expect_failure=True, ) @@ -214,7 +204,7 @@ def test_uamqp_device_messaging(self): # Error - content-type is application/json but data is not. self.cmd( "iot device c2d-message send -d {} --login {} --ct application/json --data notjson".format( - device_ids[0], LIVE_HUB_CS + device_ids[0], self.connection_string ), expect_failure=True, ) @@ -222,7 +212,7 @@ def test_uamqp_device_messaging(self): # Error - expiry is in the past. self.cmd( "iot device c2d-message send -d {} --login {} --expiry {}".format( - device_ids[0], LIVE_HUB_CS, int(time() * 1000) + device_ids[0], self.connection_string, int(time() * 1000) ), expect_failure=True, ) @@ -248,7 +238,7 @@ def test_device_messaging(self): # With connection string self.cmd( "iot device c2d-message receive -d {} --login {}".format( - device_ids[0], LIVE_HUB_CS + device_ids[0], self.connection_string ), checks=self.is_empty(), ) @@ -264,7 +254,7 @@ def test_device_messaging(self): # With connection string self.cmd( "iot device c2d-message complete -d {} --login {} -e {}".format( - device_ids[0], LIVE_HUB_CS, etag + device_ids[0], self.connection_string, etag ), expect_failure=True, ) @@ -279,7 +269,7 @@ def test_device_messaging(self): # With connection string self.cmd( "iot device c2d-message reject -d {} --login {} -e {}".format( - device_ids[0], LIVE_HUB_CS, etag + device_ids[0], self.connection_string, etag ), expect_failure=True, ) @@ -294,7 +284,7 @@ def test_device_messaging(self): # With connection string self.cmd( "iot device c2d-message abandon -d {} --login {} --etag {}".format( - device_ids[0], LIVE_HUB_CS, etag + device_ids[0], self.connection_string, etag ), expect_failure=True, ) @@ -309,7 +299,7 @@ def test_device_messaging(self): # With connection string self.cmd( "iot device simulate -d {} --login {} --mc {} --mi {} --data '{}' --rs 'complete'".format( - device_ids[0], LIVE_HUB_CS, 2, 1, "IoT Ext Test" + device_ids[0], self.connection_string, 2, 1, "IoT Ext Test" ), checks=self.is_empty(), ) @@ -324,7 +314,7 @@ def test_device_messaging(self): # With connection string self.cmd( "iot device simulate -d {} --login {} --mc {} --mi {} --data '{}' --rs 'abandon' --protocol http".format( - device_ids[0], LIVE_HUB_CS, 2, 1, "IoT Ext Test" + device_ids[0], self.connection_string, 2, 1, "IoT Ext Test" ), checks=self.is_empty(), ) @@ -380,7 +370,7 @@ def test_device_messaging(self): # With connection string self.cmd( 'iot device send-d2c-message -d {} --login {} --props "MessageId=12345;CorrelationId=54321"'.format( - device_ids[0], LIVE_HUB_CS + device_ids[0], self.connection_string ), checks=self.is_empty(), ) @@ -410,7 +400,7 @@ def test_hub_monitor_events(self): # Test with invalid connection string self.cmd( - "iot hub monitor-events -t 1 -y --login {}".format(LIVE_HUB_CS + "zzz"), + "iot hub monitor-events -t 1 -y --login {}".format(self.connection_string + "zzz"), expect_failure=True, ) @@ -443,7 +433,7 @@ def test_hub_monitor_events(self): ) # Monitor events for all devices and include sys, anno, app self.command_execute_assert( - "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 10 -y -p sys anno app".format( + "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 5 -y -p sys anno app".format( LIVE_HUB, LIVE_RG, LIVE_CONSUMER_GROUPS[0], enqueued_time ), device_ids @@ -459,7 +449,7 @@ def test_hub_monitor_events(self): # Monitor events for a single device self.command_execute_assert( - "iot hub monitor-events -n {} -g {} -d {} --cg {} --et {} -t 10 -y -p all".format( + "iot hub monitor-events -n {} -g {} -d {} --cg {} --et {} -t 5 -y -p all".format( LIVE_HUB, LIVE_RG, device_ids[0], LIVE_CONSUMER_GROUPS[1], enqueued_time ), [ @@ -475,7 +465,7 @@ def test_hub_monitor_events(self): # Monitor events with device-id wildcards self.command_execute_assert( - "iot hub monitor-events -n {} -g {} -d {} --et {} -t 10 -y -p sys anno app".format( + "iot hub monitor-events -n {} -g {} -d {} --et {} -t 5 -y -p sys anno app".format( LIVE_HUB, LIVE_RG, PREFIX_DEVICE + "*", enqueued_time ), device_ids, @@ -491,7 +481,7 @@ def test_hub_monitor_events(self): ) self.command_execute_assert( - 'iot hub monitor-events -n {} -g {} --device-query "{}" --et {} -t 10 -y -p sys anno app'.format( + 'iot hub monitor-events -n {} -g {} --device-query "{}" --et {} -t 5 -y -p sys anno app'.format( LIVE_HUB, LIVE_RG, query_string, enqueued_time ), device_subset_include, @@ -501,7 +491,7 @@ def test_hub_monitor_events(self): device_subset_exclude = device_ids[device_count // 2 :] with pytest.raises(Exception): self.command_execute_assert( - 'iot hub monitor-events -n {} -g {} --device-query "{}" --et {} -t 10 -y -p sys anno app'.format( + 'iot hub monitor-events -n {} -g {} --device-query "{}" --et {} -t 5 -y -p sys anno app'.format( LIVE_HUB, LIVE_RG, query_string, enqueued_time ), device_subset_exclude, @@ -509,8 +499,8 @@ def test_hub_monitor_events(self): # Monitor events with --login parameter self.command_execute_assert( - "iot hub monitor-events -t 10 -y -p all --cg {} --et {} --login {}".format( - LIVE_CONSUMER_GROUPS[2], enqueued_time, LIVE_HUB_CS + "iot hub monitor-events -t 5 -y -p all --cg {} --et {} --login {}".format( + LIVE_CONSUMER_GROUPS[2], enqueued_time, self.connection_string ), device_ids, ) @@ -536,7 +526,7 @@ def test_hub_monitor_events(self): # Monitor messages for ugly JSON output self.command_execute_assert( - "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 10 -y".format( + "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 5 -y".format( LIVE_HUB, LIVE_RG, LIVE_CONSUMER_GROUPS[0], enqueued_time ), ["\\r\\n"], @@ -544,7 +534,7 @@ def test_hub_monitor_events(self): # Monitor messages and parse payload as JSON with the --ct parameter self.command_execute_assert( - "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 10 --ct application/json -y".format( + "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 5 --ct application/json -y".format( LIVE_HUB, LIVE_RG, LIVE_CONSUMER_GROUPS[1], enqueued_time ), ['"payload_data1": "payload_value1"'], @@ -569,7 +559,7 @@ def test_hub_monitor_events(self): # Monitor messages for pretty JSON output self.command_execute_assert( - "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 10 -y".format( + "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 5 -y".format( LIVE_HUB, LIVE_RG, LIVE_CONSUMER_GROUPS[0], enqueued_time ), ['"payload_data1": "payload_value1"'], @@ -577,7 +567,7 @@ def test_hub_monitor_events(self): # Monitor messages with yaml output self.command_execute_assert( - "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 10 -y -o yaml".format( + "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 5 -y -o yaml".format( LIVE_HUB, LIVE_RG, LIVE_CONSUMER_GROUPS[1], enqueued_time ), ["payload_data1: payload_value1"], @@ -602,7 +592,7 @@ def test_hub_monitor_events(self): # Monitor messages to ensure it returns improperly formatted JSON self.command_execute_assert( - "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 10 -y".format( + "iot hub monitor-events -n {} -g {} --cg {} --et {} -t 5 -y".format( LIVE_HUB, LIVE_RG, LIVE_CONSUMER_GROUPS[0], enqueued_time ), ['{\\r\\n\\"payload_data1\\"\\"payload_value1\\"\\r\\n}'], @@ -669,14 +659,14 @@ def test_hub_monitor_feedback(self): ack = "positive" self.cmd( "iot device c2d-message send -d {} --login {} --ack {} -y".format( - device_ids[0], LIVE_HUB_CS, ack + device_ids[0], self.connection_string, ack ), checks=self.is_empty(), ) result = self.cmd( "iot device c2d-message receive -d {} --login {}".format( - device_ids[0], LIVE_HUB_CS + device_ids[0], self.connection_string ) ).get_output_in_json() @@ -688,13 +678,13 @@ def test_hub_monitor_feedback(self): self.cmd( "iot device c2d-message complete -d {} --login {} -e {}".format( - device_ids[0], LIVE_HUB_CS, etag + device_ids[0], self.connection_string, etag ) ) self.command_execute_assert( "iot hub monitor-feedback --login {} -w {} -d {} -y".format( - LIVE_HUB_CS, msg_id, device_ids[0] + self.connection_string, msg_id, device_ids[0] ), ["description: Success"], ) @@ -705,34 +695,34 @@ def test_hub_monitor_feedback(self): # Create some noise self.cmd( "iot device c2d-message send -d {} --login {} --ack {} -y".format( - device_ids[0], LIVE_HUB_CS, ack + device_ids[0], self.connection_string, ack ), checks=self.is_empty(), ) result = self.cmd( "iot device c2d-message receive -d {} --login {}".format( - device_ids[0], LIVE_HUB_CS + device_ids[0], self.connection_string ) ).get_output_in_json() etag = result["etag"] self.cmd( "iot device c2d-message reject -d {} --login {} -e {}".format( - device_ids[0], LIVE_HUB_CS, etag + device_ids[0], self.connection_string, etag ) ) # Target message self.cmd( "iot device c2d-message send -d {} --login {} --ack {} -y".format( - device_ids[0], LIVE_HUB_CS, ack + device_ids[0], self.connection_string, ack ), checks=self.is_empty(), ) result = self.cmd( "iot device c2d-message receive -d {} --login {}".format( - device_ids[0], LIVE_HUB_CS + device_ids[0], self.connection_string ) ).get_output_in_json() @@ -744,11 +734,11 @@ def test_hub_monitor_feedback(self): self.cmd( "iot device c2d-message reject -d {} --login {} -e {}".format( - device_ids[0], LIVE_HUB_CS, etag + device_ids[0], self.connection_string, etag ) ) self.command_execute_assert( - "iot hub monitor-feedback --login {} -w {} -y".format(LIVE_HUB_CS, msg_id), + "iot hub monitor-feedback --login {} -w {} -y".format(self.connection_string, msg_id), ["description: Message rejected"], ) diff --git a/linter_exclusions.yml b/linter_exclusions.yml new file mode 100644 index 000000000..83b77abf8 --- /dev/null +++ b/linter_exclusions.yml @@ -0,0 +1,15 @@ +dt endpoint create servicebus: + parameters: + servicebus_resource_group: + rule_exclusions: + - parameter_should_not_end_in_resource_group +dt endpoint create eventgrid: + parameters: + eventgrid_resource_group: + rule_exclusions: + - parameter_should_not_end_in_resource_group +dt endpoint create eventhub: + parameters: + eventhub_resource_group: + rule_exclusions: + - parameter_should_not_end_in_resource_group From 78fb3535efab3f8dccd8e6f66a5f712d44f4e33f Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 28 Jul 2020 10:02:03 -0700 Subject: [PATCH 070/179] Remove pytest.ini.example entries. Alter Credscan job. --- .azure-devops/merge.yml | 16 ++++++++++++++-- pytest.ini.example | 3 --- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 5ade5c845..adcaaffeb 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -129,7 +129,19 @@ jobs: - job: CredScan displayName: "Credential Scan" + pool: + vmImage: "windows-2019" steps: - - task: CredScan@3 + - task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-credscan.CredScan@2 + displayName: 'Run Credential Scanner' + inputs: + toolMajorVersion: V2 + - task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-postanalysis.PostAnalysis@1 + displayName: 'Post Analysis' inputs: - scanFolder: '$(Build.SourcesDirectory)' + AllTools: false + BinSkim: false + CredScan: true + RoslynAnalyzers: false + TSLint: false + ToolLogsNotFoundAction: 'Standard' diff --git a/pytest.ini.example b/pytest.ini.example index 548e1ca1f..ac1def042 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -15,13 +15,10 @@ env = AZURE_TEST_RUN_LIVE=True azext_iot_testrg= azext_iot_testhub= - azext_iot_testhub_cs= azext_iot_testdps= azext_iot_teststorageuri= azext_iot_testidentity= azext_iot_central_app_id= - azext_dt_rbac_assignee_owner= - azext_dt_rbac_assignee_reader= azext_dt_ep_eventgrid_topic= azext_dt_ep_servicebus_namespace= azext_dt_ep_servicebus_policy= From 0d08dacc077971b6daaec8f737b9beaf49d2081a Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 28 Jul 2020 10:31:57 -0700 Subject: [PATCH 071/179] Remove credscan for now. Azure organization does not support credscan task. --- .azure-devops/merge.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index adcaaffeb..4d63debaf 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -126,22 +126,3 @@ jobs: source ./$(iot_ext_venv)/bin/activate cp ./linter_exclusions.yml $AZURE_EXTENSION_DIR/$(iot_ext_package)/ azdev linter --include-whl-extensions $(iot_ext_package) --min-severity medium - -- job: CredScan - displayName: "Credential Scan" - pool: - vmImage: "windows-2019" - steps: - - task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-credscan.CredScan@2 - displayName: 'Run Credential Scanner' - inputs: - toolMajorVersion: V2 - - task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-postanalysis.PostAnalysis@1 - displayName: 'Post Analysis' - inputs: - AllTools: false - BinSkim: false - CredScan: true - RoslynAnalyzers: false - TSLint: false - ToolLogsNotFoundAction: 'Standard' From 95cd8a81e0de16005bf00bc98695c720e154e144 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 28 Jul 2020 11:19:06 -0700 Subject: [PATCH 072/179] Re-add credscan after reconfiguring devops environment. --- .azure-devops/merge.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 4d63debaf..5ade5c845 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -126,3 +126,10 @@ jobs: source ./$(iot_ext_venv)/bin/activate cp ./linter_exclusions.yml $AZURE_EXTENSION_DIR/$(iot_ext_package)/ azdev linter --include-whl-extensions $(iot_ext_package) --min-severity medium + +- job: CredScan + displayName: "Credential Scan" + steps: + - task: CredScan@3 + inputs: + scanFolder: '$(Build.SourcesDirectory)' From 2e06d0bbfa23b58decce6e9a042fcfc04a134138 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 28 Jul 2020 11:57:13 -0700 Subject: [PATCH 073/179] +dependsOn for command table linter. --- .azure-devops/merge.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 5ade5c845..6c78d30b7 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -109,6 +109,7 @@ jobs: # TODO: Evaluate this style or similar alternative for setting up CLI env - job: 'run_azdev_linter_on_command_table' + dependsOn: ['build_and_publish_azure_iot_cli_ext'] displayName: 'Evaluate IoT extension command table' pool: vmImage: 'Ubuntu-16.04' From 2eca8a88dd1c2190d597573c7a9dc5f56dcf9371 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 28 Jul 2020 13:32:58 -0700 Subject: [PATCH 074/179] Minor help text adjustments for PnP based on feedback. --- azext_iot/pnp/_help.py | 10 +++++----- azext_iot/pnp/params.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/azext_iot/pnp/_help.py b/azext_iot/pnp/_help.py index 2ef82f2d2..f3f7aac1c 100644 --- a/azext_iot/pnp/_help.py +++ b/azext_iot/pnp/_help.py @@ -49,9 +49,9 @@ def load_pnp_help(): text: > az iot pnp role-assignment list --resource-id {tenant_id} --resource-type Tenant - - name: List role assignments for a specific model and subject. + - name: List role assignments for a specific model "dtmi:com:example:ClimateSensor;1" and subject. text: > - az iot pnp role-assignment list --resource-id {model_id} + az iot pnp role-assignment list --resource-id "dtmi:com:example:ClimateSensor;1" --resource-type Model --subject-id {user_or_spn_id} """ @@ -103,7 +103,7 @@ def load_pnp_help(): examples: - name: Create a new model by uploading a JSON file text: > - az iot pnp model create --model {path\\to\\definition\\file.json} + az iot pnp model create --model ./path/to/definition/file.json """ helps["iot pnp model show"] = """ @@ -111,9 +111,9 @@ def load_pnp_help(): short-summary: View a device model by ID examples: - - name: View a model with the ID {dtmi:my:model} + - name: View a model with the ID "dtmi:com:example:ClimateSensor;1" text: > - az iot pnp model show --dtmi {dtmi:my:model} + az iot pnp model show --dtmi "dtmi:com:example:ClimateSensor;1" """ helps["iot pnp model list"] = """ diff --git a/azext_iot/pnp/params.py b/azext_iot/pnp/params.py index aa147e36e..84ad6b18e 100644 --- a/azext_iot/pnp/params.py +++ b/azext_iot/pnp/params.py @@ -63,7 +63,7 @@ def load_pnp_arguments(self, _): context.argument( "model_id", options_list=["--model-id", "--dtmi"], - help="Digital Twins model Id. Example: dtmi:example:Room;2", + help="Digital Twins model Id. Example: dtmi:com:example:Room;2", ) with self.argument_context("iot pnp model create") as context: context.argument( From ddc6fc83e5e4310d2b82a6c621415b317be66f54 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 28 Jul 2020 14:03:55 -0700 Subject: [PATCH 075/179] Temp disable non-callable check. --- .pylintrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index b7bf67326..3bfaf5a3d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -278,7 +278,8 @@ disable=import-outside-toplevel, xreadlines-attribute, deprecated-sys-function, exception-escape, - comprehension-escape + comprehension-escape, + not-callable # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -337,7 +338,6 @@ enable=syntax-error, bad-super-call, missing-super-argument, no-member, - not-callable, assignment-from-no-return, no-value-for-parameter, too-many-function-args, From 42027464116fdb13ba95e6fa7ec394c8d545cbc6 Mon Sep 17 00:00:00 2001 From: valluriraj Date: Thu, 30 Jul 2020 14:42:11 -0700 Subject: [PATCH 076/179] add support user show, list and delete (#222) * add support user show, list and delete * address review comments * address review comments * address review comments Co-authored-by: Raj valluri --- azext_iot/central/_help.py | 49 +++++++++++-- azext_iot/central/command_map.py | 3 + azext_iot/central/commands_user.py | 26 +++++++ azext_iot/central/params.py | 4 +- azext_iot/central/providers/user_provider.py | 32 +++++++++ azext_iot/central/services/user.py | 74 ++++++++++++++++++++ azext_iot/tests/test_iot_central_int.py | 51 +++++++++++++- 7 files changed, 231 insertions(+), 8 deletions(-) diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 7f7822d2e..0103bbfb1 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -193,25 +193,64 @@ def _load_central_users_help(): "iot central app user create" ] = """ type: command - short-summary: Add a user to the app + short-summary: Add a user to the application examples: - - name: Add a user with email to the app + - name: Add a user by email to the application text: > az iot central app user create - --id {userId} + --user-id {userId} --app-id {appId} --email {emailAddress} --role admin - - name: Add a service-principal to the app + - name: Add a service-principal to the application text: > az iot central app user create - --id {userId} + --user-id {userId} --app-id {appId} --tenant-id {tenantId} --object-id {objectId} --role operator """ + helps[ + "iot central app user show" + ] = """ + type: command + short-summary: Get the details of a user by ID + examples: + - name: Get details of user + text: > + az iot central app user show + --app-id {appid} + --user-id {userId} + """ + + helps[ + "iot central app user delete" + ] = """ + type: command + short-summary: Delete a user from the application + examples: + - name: Delete a user + text: > + az iot central app user delete + --app-id {appid} + --user-id {userId} + + """ + + helps[ + "iot central app user list" + ] = """ + type: command + short-summary: Get list of users in an application + examples: + - name: List of users + text: > + az iot central app user list + --app-id {appid} + + """ def _load_central_device_templates_help(): diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index d50d63d7c..1adc55468 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -45,6 +45,9 @@ def load_central_commands(self, _): "iot central app user", command_type=central_user_ops, is_preview=True, ) as cmd_group: cmd_group.command("create", "add_user") + cmd_group.command("list", "list_users") + cmd_group.command("show", "get_user") + cmd_group.command("delete", "delete_user") with self.command_group( "iot central app device", command_type=central_device_ops, is_preview=True, diff --git a/azext_iot/central/commands_user.py b/azext_iot/central/commands_user.py index bdc076fbf..63841f35a 100644 --- a/azext_iot/central/commands_user.py +++ b/azext_iot/central/commands_user.py @@ -39,3 +39,29 @@ def add_user( role=Role[role], central_dns_suffix=central_dns_suffix, ) + + +def list_users( + cmd, app_id: str, token=None, central_dns_suffix=CENTRAL_ENDPOINT, +): + provider = CentralUserProvider(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, +): + provider = CentralUserProvider(cmd=cmd, app_id=app_id, token=token) + + 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, +): + provider = CentralUserProvider(cmd=cmd, app_id=app_id, token=token) + + return provider.delete_user( + assignee=assignee, central_dns_suffix=central_dns_suffix + ) diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 2efd7c892..21f4f5ef4 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -101,8 +101,8 @@ def load_central_arguments(self, _): ) context.argument( "assignee", - options_list=["--id", "--assignee"], - help="ID that will be associated with user being added to app. ", + options_list=["--user-id", "--assignee"], + help="ID associated with the user. ", ) context.argument( "email", diff --git a/azext_iot/central/providers/user_provider.py b/azext_iot/central/providers/user_provider.py index f5ccf1352..f69ef1903 100644 --- a/azext_iot/central/providers/user_provider.py +++ b/azext_iot/central/providers/user_provider.py @@ -57,6 +57,38 @@ def add_service_principal( central_dns_suffix=central_dns_suffix, ) + def get_user_list( + self, central_dns_suffix=CENTRAL_ENDPOINT, + ): + return central_services.user.get_user_list( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + ) + + def get_user( + self, assignee, central_dns_suffix=CENTRAL_ENDPOINT, + ): + 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, + ) + + def delete_user( + self, assignee, central_dns_suffix=CENTRAL_ENDPOINT, + ): + 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, + ) + def add_email( self, assignee: str, diff --git a/azext_iot/central/services/user.py b/azext_iot/central/services/user.py index 2502ffcc0..40b84987f 100644 --- a/azext_iot/central/services/user.py +++ b/azext_iot/central/services/user.py @@ -91,3 +91,77 @@ def add_email( response = requests.put(url, headers=headers, json=payload) return _utility.try_extract_result(response) + + +def get_user_list( + cmd, app_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + Get the list of users for central app. + + 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 ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + users: dict + """ + url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) + + headers = _utility.get_headers(token, cmd) + + response = requests.get(url, headers=headers) + return _utility.try_extract_result(response) + + +def get_user( + cmd, app_id: str, token: str, assignee: str, central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + Get information for the specified user. + + 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 ...') + assignee: unique ID of the user + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + users: dict + """ + url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, assignee) + + headers = _utility.get_headers(token, cmd) + + response = requests.get(url, headers=headers) + return _utility.try_extract_result(response) + + +def delete_user( + cmd, app_id: str, token: str, assignee: str, central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + delete user from theapp. + + 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 ...') + assignee: unique ID of the user + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + users: dict + """ + url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, assignee) + + headers = _utility.get_headers(token, cmd) + + response = requests.delete(url, headers=headers) + return _utility.try_extract_result(response) diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index c54e0ca79..e8db49adc 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -13,7 +13,7 @@ from azure.iot.device import Message from azext_iot.common import utility -from azext_iot.central.models.enum import DeviceStatus +from azext_iot.central.models.enum import DeviceStatus, Role from azext_iot.monitor.parsers import strings from . import CaptureOutputLiveScenarioTest, helpers @@ -220,6 +220,27 @@ def test_central_device_methods_CRD(self): self._delete_device(device_id) + def test_central_user_methods_CRD(self): + users = self._create_users() + + self.cmd( + "iot central app user show --app-id {} --user-id {}".format( + APP_ID, users[0].get("id") + ), + ) + + result = self.cmd( + "iot central app user list --app-id {}".format(APP_ID,), + ).get_output_in_json() + + user_list = result.get("value") + + for user in users: + self._delete_user(user.get("id")) + + for user in users: + assert user in user_list + def test_central_device_template_methods_CRD(self): # currently: create, show, list, delete (template_id, template_name) = self._create_device_template() @@ -400,6 +421,34 @@ def _create_device(self, **kwargs) -> (str, str): self.cmd(command, checks=checks) return (device_id, device_name) + def _create_users(self,): + + users = [] + for role in Role: + user_id = self.create_random_name(prefix="aztest", length=24) + email = user_id + "@microsoft.com" + command = "iot central app user create --app-id {} --user-id {} -r {} --email {}".format( + APP_ID, user_id, role.name, email, + ) + + checks = [ + self.check("id", user_id), + self.check("email", email), + self.check("type", "EmailUser"), + self.check("roles[0].role", role.value), + ] + users.append(self.cmd(command, checks=checks).get_output_in_json()) + + return users + + def _delete_user(self, user_id) -> None: + self.cmd( + "iot central app user delete --app-id {} --user-id {}".format( + APP_ID, user_id + ), + checks=[self.check("result", "success")], + ) + def _wait_for_provisioned(self, device_id): command = "iot central app device show --app-id {} -d {}".format( APP_ID, device_id From 795f851d32cd2d5660b5f66269f3bca7866c8b93 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 30 Jul 2020 15:46:04 -0700 Subject: [PATCH 077/179] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2696419e..ee3b8886c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Microsoft Azure IoT extension for Azure CLI ![Python](https://img.shields.io/pypi/pyversions/azure-cli.svg?maxAge=2592000) -[![Build Status](https://dev.azure.com/azure/azure-iot-cli-extension/_apis/build/status/Merge%20-%20Azure.azure-iot-cli-extension?branchName=dev)](https://dev.azure.com/azure/azure-iot-cli-extension/_build/latest?definitionId=49&branchName=dev) +[![Build Status](https://dev.azure.com/azureiotdevxp/aziotcli/_apis/build/status/Merge%20-%20Azure.azure-iot-cli-extension?branchName=dev)](https://dev.azure.com/azureiotdevxp/aziotcli/_build/latest?definitionId=8&branchName=dev) 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). From 17b60687feaaa991c44e5fba42337affa6dc6921 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 30 Jul 2020 21:36:54 -0700 Subject: [PATCH 078/179] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ee3b8886c..5510ece54 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Microsoft Azure IoT extension for Azure CLI ![Python](https://img.shields.io/pypi/pyversions/azure-cli.svg?maxAge=2592000) -[![Build Status](https://dev.azure.com/azureiotdevxp/aziotcli/_apis/build/status/Merge%20-%20Azure.azure-iot-cli-extension?branchName=dev)](https://dev.azure.com/azureiotdevxp/aziotcli/_build/latest?definitionId=8&branchName=dev) +![Build Status](https://dev.azure.com/azureiotdevxp/aziotcli/_apis/build/status/Merge%20-%20Azure.azure-iot-cli-extension?branchName=dev) + 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). From fde925cc0686323c7b5d036ef225deceb2d252cd Mon Sep 17 00:00:00 2001 From: Sapan Saxena <31940305+anusapan@users.noreply.github.com> Date: Fri, 31 Jul 2020 11:59:04 -0700 Subject: [PATCH 079/179] Allow blob sas uri from file as input (#224) * Allow input sas uri from file * address review comments --- azext_iot/_help.py | 14 ++++++++++++++ azext_iot/_params.py | 14 ++++++++------ azext_iot/operations/hub.py | 9 +++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 66cfa937e..4e4f3990e 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -197,6 +197,13 @@ short-summary: Export all device identities from an IoT Hub to an Azure Storage blob container. long-summary: For more information, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities + examples: + - name: Export all device identities to a configured blob container using an inline SAS uri. + text: > + az iot hub device-identity export -n {iothub_name} --bcu {sas_uri} + - name: Export all device identities to a configured blob container using a file path which contains SAS uri. + text: > + az iot hub device-identity export -n {iothub_name} --bcu {sas_uri_filepath} """ helps[ @@ -206,6 +213,13 @@ short-summary: Import device identities to an IoT Hub from a blob. long-summary: For more information, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities + examples: + - name: Import all device identities from a blob using an inline SAS uri. + text: > + az iot hub device-identity import -n {iothub_name} --ibcu {input_sas_uri} --obcu {output_sas_uri} + - 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[ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index de588c10b..b4fd0e477 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -381,8 +381,8 @@ def load_arguments(self, _): options_list=["--blob-container-uri", "--bcu"], 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, you must supply an " - "https:// URI.", + "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.", ) context.argument( "include_keys", @@ -404,16 +404,18 @@ def load_arguments(self, _): options_list=["--input-blob-container-uri", "--ibcu"], 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, you must supply an " - "https:// URI", + "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.", ) context.argument( "output_blob_container_uri", options_list=["--output-blob-container-uri", "--obcu"], 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, you must supply an " - "https:// URI", + "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.", ) context.argument( "storage_authentication_type", diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 514f675b1..113ac5f49 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -2099,6 +2099,9 @@ def iot_device_export( hub_name=hub_name, resource_group_name=resource_group_name ) + if exists(blob_container_uri): + blob_container_uri = read_file_content(blob_container_uri) + if ensure_min_version(iot_sdk_version, "0.12.0"): from azure.mgmt.iothub.models import ExportDevicesRequest from azext_iot.common.shared import AuthenticationType @@ -2145,6 +2148,12 @@ def iot_device_import( hub_name=hub_name, resource_group_name=resource_group_name ) + if exists(input_blob_container_uri): + input_blob_container_uri = read_file_content(input_blob_container_uri) + + if exists(output_blob_container_uri): + output_blob_container_uri = read_file_content(output_blob_container_uri) + if ensure_min_version(iot_sdk_version, "0.12.0"): from azure.mgmt.iothub.models import ImportDevicesRequest from azext_iot.common.shared import AuthenticationType From 7f0df79a1a1c2f082834e9c9b7f85a178760e0bf Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 31 Jul 2020 16:21:23 -0700 Subject: [PATCH 080/179] Improve IotHub discovery pattern (#225) * Improve IotHub discovery pattern. * Add more type hints. * Update mock paths. --- azext_iot/iothub/providers/discovery.py | 84 ++++++++----- azext_iot/tests/conftest.py | 6 +- .../tests/iothub/test_iothub_discovery_int.py | 118 ++++++++++++++++++ azext_iot/tests/test_iot_ext_unit.py | 3 - 4 files changed, 179 insertions(+), 32 deletions(-) create mode 100644 azext_iot/tests/iothub/test_iothub_discovery_int.py diff --git a/azext_iot/iothub/providers/discovery.py b/azext_iot/iothub/providers/discovery.py index 195388e8b..296a8c526 100644 --- a/azext_iot/iothub/providers/discovery.py +++ b/azext_iot/iothub/providers/discovery.py @@ -9,7 +9,7 @@ from azext_iot.common.utility import trim_from_start from azext_iot.iothub.models.iothub_target import IotHubTarget from azext_iot._factory import iot_hub_service_factory -from typing import Dict +from typing import Dict, List PRIVILEDGED_ACCESS_RIGHTS_SET = set( ["RegistryWrite", "ServiceConnect", "DeviceConnect"] @@ -25,8 +25,9 @@ def __init__(self, cmd): self.cmd = cmd self.client = None self.sub_id = "unknown" + self._initialize_client() - def _self_initialize_client(self): + def _initialize_client(self): if not self.client: if getattr(self.cmd, "cli_ctx", None): self.client = iot_hub_service_factory(self.cmd.cli_ctx) @@ -34,9 +35,38 @@ def _self_initialize_client(self): self.client = self.cmd self.sub_id = self.client.config.subscription_id - def find_iothub(self, hub_name, rg=None): + def get_iothubs(self, rg: str = None) -> List: + hubs_list = [] + + if not rg: + hubs_pager = self.client.list_by_subscription() + else: + hubs_pager = self.client.list_by_resource_group(resource_group_name=rg) + + try: + while True: + hubs_list.extend(hubs_pager.advance_page()) + except StopIteration: + pass + + return hubs_list + + def get_policies(self, hub_name: str, rg: str) -> List: + policy_pager = self.client.list_keys( + resource_group_name=rg, resource_name=hub_name + ) + policy_list = [] + + try: + while True: + policy_list.extend(policy_pager.advance_page()) + except StopIteration: + pass + + return policy_list + + def find_iothub(self, hub_name: str, rg: str = None): from azure.mgmt.iothub.models import ErrorDetailsException - self._self_initialize_client() if rg: try: @@ -48,14 +78,7 @@ def find_iothub(self, hub_name, rg=None): ) ) - hubs_pager = self.client.list_by_subscription() - hubs_list = [] - - try: - while True: - hubs_list.extend(hubs_pager.advance_page()) - except StopIteration: - pass + hubs_list = self.get_iothubs() if hubs_list: target_hub = next( @@ -70,29 +93,20 @@ def find_iothub(self, hub_name, rg=None): ) ) - def find_policy(self, hub_name, rg, policy_name="auto"): - self._self_initialize_client() - + def find_policy(self, hub_name: str, rg: str, policy_name: str = "auto"): if policy_name.lower() != "auto": return self.client.get_keys_for_key_name( resource_group_name=rg, resource_name=hub_name, key_name=policy_name ) - policy_pager = self.client.list_keys( - resource_group_name=rg, resource_name=hub_name - ) - policy_list = [] - - try: - while True: - policy_list.extend(policy_pager.advance_page()) - except StopIteration: - pass + policy_list = self.get_policies(hub_name=hub_name, rg=rg) for policy in policy_list: rights_set = set(policy.rights.split(", ")) if PRIVILEDGED_ACCESS_RIGHTS_SET == rights_set: - logger.info("Using policy '%s' for IoT Hub interaction.", policy.key_name) + logger.info( + "Using policy '%s' for IoT Hub interaction.", policy.key_name + ) return policy raise CLIError( @@ -107,7 +121,7 @@ def find_policy(self, hub_name, rg, policy_name="auto"): def get_target_by_cstring(cls, connection_string: str) -> IotHubTarget: return IotHubTarget.from_connection_string(cstring=connection_string).as_dict() - def get_target(self, hub_name, rg=None, **kwargs) -> Dict[str, str]: + def get_target(self, hub_name: str, rg: str = None, **kwargs) -> Dict[str, str]: cstring = kwargs.get("login") if cstring: return self.get_target_by_cstring(connection_string=cstring) @@ -130,8 +144,22 @@ def get_target(self, hub_name, rg=None, **kwargs) -> Dict[str, str]: include_events=include_events, ) + def get_targets(self, rg: str = None, **kwargs) -> List[Dict[str, str]]: + targets = [] + hubs = self.get_iothubs(rg=rg) + if hubs: + for hub in hubs: + targets.append( + self.get_target(hub_name=hub.name, rg=self._get_rg(hub), **kwargs) + ) + + return targets + + def _get_rg(self, hub): + return hub.additional_properties.get("resourcegroup") + def _build_target( - self, iothub, policy, key_type=None, include_events=False + self, iothub, policy, key_type: str = None, include_events=False ) -> Dict[str, str]: # This is more or less a compatibility function which produces the # same result as _azure.get_iot_hub_connection_string() diff --git a/azext_iot/tests/conftest.py b/azext_iot/tests/conftest.py index aab443b5d..438607886 100644 --- a/azext_iot/tests/conftest.py +++ b/azext_iot/tests/conftest.py @@ -14,9 +14,10 @@ from azure.cli.core.commands import AzCliCommand from azure.cli.core.mock import DummyCli -path_iot_hub_service_factory = "azext_iot.common._azure.iot_hub_service_factory" +path_iot_hub_service_factory = "azext_iot._factory.iot_hub_service_factory" path_service_client = "msrest.service_client.ServiceClient.send" path_ghcs = "azext_iot.iothub.providers.discovery.IotHubDiscovery.get_target" +path_discovery_init = "azext_iot.iothub.providers.discovery.IotHubDiscovery._initialize_client" path_sas = "azext_iot._factory.SasTokenAuthentication" path_mqtt_client = "azext_iot.operations._mqtt.mqtt.Client" path_iot_hub_monitor_events_entrypoint = ( @@ -85,6 +86,9 @@ def serviceclient_generic_error(mocker, fixture_ghcs, fixture_sas, request): def fixture_ghcs(mocker): ghcs = mocker.patch(path_ghcs) ghcs.return_value = mock_target + mocker.patch(path_iot_hub_service_factory) + mocker.patch(path_discovery_init) + return ghcs diff --git a/azext_iot/tests/iothub/test_iothub_discovery_int.py b/azext_iot/tests/iothub/test_iothub_discovery_int.py new file mode 100644 index 000000000..16927cd51 --- /dev/null +++ b/azext_iot/tests/iothub/test_iothub_discovery_int.py @@ -0,0 +1,118 @@ +# 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.iothub.providers.discovery import ( + IotHubDiscovery, + PRIVILEDGED_ACCESS_RIGHTS_SET, +) +from azext_iot.tests import IoTLiveScenarioTest +from ..settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC, Setting + +settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg + + +class TestIoTHubDiscovery(IoTLiveScenarioTest): + def __init__(self, test_case): + super(TestIoTHubDiscovery, self).__init__(test_case, LIVE_HUB, LIVE_RG) + self.cmd_shell = Setting() + setattr(self.cmd_shell, "cli_ctx", self.cli_ctx) + self.desired_policy_name = "iothubowner" + + def test_iothub_discovery(self): + discovery = IotHubDiscovery(self.cmd_shell) + + iothub = discovery.find_iothub(hub_name=LIVE_HUB) + assert iothub.name == LIVE_HUB + + auto_policy = discovery.find_policy(hub_name=LIVE_HUB, rg=LIVE_RG).as_dict() + rights_set = set(auto_policy["rights"].split(", ")) + assert rights_set == PRIVILEDGED_ACCESS_RIGHTS_SET + + # Assumption - Test Iothub includes the vanilla iothubowner policy + desired_policy = discovery.find_policy( + hub_name=LIVE_HUB, rg=LIVE_RG, policy_name=self.desired_policy_name + ).as_dict() + assert desired_policy["key_name"] == self.desired_policy_name + + policies = discovery.get_policies(hub_name=LIVE_HUB, rg=LIVE_RG) + assert len(policies) + + # Example for leveraging discovery to build cstring for every policy on target IotHub + cstrings = [discovery._build_target(iothub=iothub, policy=p)["cs"] for p in policies] + assert len(cstrings) + + sub_hubs = discovery.get_iothubs() + assert sub_hubs + + filtered_sub_hubs = [ + hub for hub in sub_hubs if hub.as_dict()["name"] == LIVE_HUB + ] + assert filtered_sub_hubs + + rg_hubs = discovery.get_iothubs(rg=LIVE_RG) + assert rg_hubs + + filtered_rg_hubs = [hub for hub in rg_hubs if hub.as_dict()["name"] == LIVE_HUB] + assert filtered_rg_hubs + + assert len(rg_hubs) <= len(sub_hubs) + + def test_iothub_targets(self): + discovery = IotHubDiscovery(self.cmd_shell) + + cs_target1 = discovery.get_target_by_cstring(self.connection_string) + assert_target(cs_target1, True) + + cs_target2 = discovery.get_target(hub_name=None, login=self.connection_string) + assert_target(cs_target2, True) + + auto_target = discovery.get_target(hub_name=LIVE_HUB) + assert_target(auto_target, rg=LIVE_RG) + + auto_target = discovery.get_target(hub_name=LIVE_HUB, rg=LIVE_RG) + assert_target(auto_target, rg=LIVE_RG) + + desired_target = discovery.get_target( + hub_name=LIVE_HUB, policy_name=self.desired_policy_name, include_events=True + ) + assert_target(desired_target, rg=LIVE_RG, include_events=True) + + sub_targets = discovery.get_targets() + [assert_target(tar) for tar in sub_targets] + + rg_targets = discovery.get_targets(rg=LIVE_RG, include_events=True) + [assert_target(tar, rg=LIVE_RG, include_events=True) for tar in rg_targets] + + assert len(rg_targets) <= len(sub_targets) + + +def assert_target(target: dict, by_cstring=False, include_events=False, **kwargs): + assert target["cs"] + assert target["policy"] + assert target["primarykey"] + assert target["entity"] + + if not by_cstring: + assert target["secondarykey"] + assert target["subscription"] + + if "rg" in kwargs: + assert target["resourcegroup"] == kwargs["rg"] + + assert target["location"] + assert target["sku_tier"] + assert target["secondarykey"] + + if include_events: + assert target["events"] + events = target["events"] + assert events["endpoint"] + assert events["partition_count"] + assert events["path"] + assert events["partition_ids"] + assert isinstance(events["partition_ids"], list) diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index 0c9138dc0..cb4782079 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -48,9 +48,6 @@ mock_target["cs"] = generate_cs() -# Patch Paths # -path_iot_hub_service_factory = "azext_iot.common._azure.iot_hub_service_factory" - # TODO generalize all fixtures across DPS/Hub unit tests From 2381f8a3d9e5fa28896effac7a21447ecb446613 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 4 Aug 2020 16:38:17 -0700 Subject: [PATCH 081/179] Remove history from package long_description. --- setup.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 37e8ee765..8b1a03495 100644 --- a/setup.py +++ b/setup.py @@ -64,18 +64,13 @@ "License :: OSI Approved :: MIT License", ] -with open("HISTORY.rst", "r", encoding="utf-8") as f: - HISTORY = f.read() - short_description = "The Azure IoT extension for Azure CLI." setup( name=PACKAGE_NAME, version=VERSION, description=short_description, - long_description="{} Intended for power users and/or automation of IoT solutions at scale.".format(short_description) - + "\n\n" - + HISTORY, + long_description="{} Intended for power users and/or automation of IoT solutions at scale.".format(short_description), license="MIT", author="Microsoft", author_email="iotupx@microsoft.com", # +@digimaun From 880481a1a7f2cb6f7cab98517346def55e83aa62 Mon Sep 17 00:00:00 2001 From: valluriraj Date: Wed, 5 Aug 2020 10:23:47 -0700 Subject: [PATCH 082/179] Add api token commands (#226) * add support user show, list and delete * address review comments * address review comments * address review comments * initial commit * address review comments * address review commentes Co-authored-by: Raj valluri --- azext_iot/central/_help.py | 66 +++++++++ azext_iot/central/command_map.py | 14 ++ azext_iot/central/commands_api_token.py | 53 ++++++++ azext_iot/central/params.py | 6 + azext_iot/central/providers/__init__.py | 2 + .../central/providers/api_token_provider.py | 78 +++++++++++ azext_iot/central/services/__init__.py | 4 +- azext_iot/central/services/api_token.py | 125 ++++++++++++++++++ azext_iot/tests/test_iot_central_int.py | 51 +++++++ 9 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 azext_iot/central/commands_api_token.py create mode 100644 azext_iot/central/providers/api_token_provider.py create mode 100644 azext_iot/central/services/api_token.py diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 0103bbfb1..5430440cc 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -29,6 +29,7 @@ def load_central_help(): _load_central_devices_help() _load_central_users_help() + _load_central_api_token_help() _load_central_device_templates_help() _load_central_device_twin_help() _load_central_monitors_help() @@ -253,6 +254,71 @@ def _load_central_users_help(): """ +def _load_central_api_token_help(): + helps[ + "iot central app api-token" + ] = """ + type: group + short-summary: Create and Manage API tokens . + """ + + helps[ + "iot central app api-token create" + ] = """ + type: command + short-summary: Create a new API token in the application + long-summary: The only time you will see the value of this token is when creating the token. Ensure you store this token somewhere securely, as if you lose it, you will need to create another. + examples: + - name: Add new API token + text: > + az iot central app api-token create + --token-id {tokenId} + --app-id {appId} + --role admin + + """ + helps[ + "iot central app api-token show" + ] = """ + type: command + short-summary: Get token meta data (e.g. role as a GUID, expiration) + long-summary: API token information contains basic information about the token and does not include the value of the token. + examples: + - name: Get API token + text: > + az iot central app api-token show + --app-id {appid} + --token-id {tokenId} + """ + + helps[ + "iot central app api-token delete" + ] = """ + type: command + short-summary: Delete an API token from the application + examples: + - name: Delete an API token + text: > + az iot central app api-token delete + --app-id {appid} + --token-id {tokenId} + """ + + helps[ + "iot central app api-token list" + ] = """ + type: command + short-summary: Get a list of all token meta data (e.g. Role as a GUID and expiration) + long-summary: Information in the list contains basic information about the tokens in the application and does not include token values. + examples: + - name: List of API tokens + text: > + az iot central app api-token list + --app-id {appid} + + """ + + def _load_central_device_templates_help(): helps[ "iot central app device-template" diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index 1adc55468..f1c860342 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -27,6 +27,10 @@ central_user_ops = CliCommandType(operations_tmpl="azext_iot.central.commands_user#{}") +central_api_token_ops = CliCommandType( + operations_tmpl="azext_iot.central.commands_api_token#{}" +) + # Dev note - think of this as the "router" and all self.command_group as the controllers def load_central_commands(self, _): @@ -49,6 +53,16 @@ def load_central_commands(self, _): cmd_group.command("show", "get_user") cmd_group.command("delete", "delete_user") + with self.command_group( + "iot central app api-token", + command_type=central_api_token_ops, + is_preview=True, + ) as cmd_group: + cmd_group.command("create", "add_api_token") + cmd_group.command("list", "list_api_tokens") + cmd_group.command("show", "get_api_token") + cmd_group.command("delete", "delete_api_token") + with self.command_group( "iot central app device", command_type=central_device_ops, is_preview=True, ) as cmd_group: diff --git a/azext_iot/central/commands_api_token.py b/azext_iot/central/commands_api_token.py new file mode 100644 index 000000000..e8111f1a6 --- /dev/null +++ b/azext_iot/central/commands_api_token.py @@ -0,0 +1,53 @@ +# 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. +# -------------------------------------------------------------------------------------------- +# Dev note - think of this as a controller + + +from azext_iot.constants import CENTRAL_ENDPOINT +from azext_iot.central.providers import CentralApiTokenProvider +from azext_iot.central.models.enum import Role + + +def add_api_token( + cmd, + app_id: str, + token_id: str, + role: str, + token=None, + central_dns_suffix=CENTRAL_ENDPOINT, +): + provider = CentralApiTokenProvider(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, +): + provider = CentralApiTokenProvider(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, +): + provider = CentralApiTokenProvider(cmd=cmd, app_id=app_id, token=token) + + return provider.get_api_token( + 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, +): + provider = CentralApiTokenProvider(cmd=cmd, app_id=app_id, token=token) + + return provider.delete_api_token( + token_id=token_id, central_dns_suffix=central_dns_suffix + ) diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 21f4f5ef4..736aa016b 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -120,6 +120,12 @@ def load_central_arguments(self, _): help="Tenant ID for service principal to be added to the app. Object ID must also be specified. ", ) + context.argument( + "token_id", + options_list=["--token-id", "--tkid"], + help="Unique ID for the API token. ", + ) + with self.argument_context("iot central app monitor-events") as context: context.argument("timeout", arg_type=event_timeout_type) context.argument("properties", arg_type=event_msg_prop_type) diff --git a/azext_iot/central/providers/__init__.py b/azext_iot/central/providers/__init__.py index 000212e37..451120fe6 100644 --- a/azext_iot/central/providers/__init__.py +++ b/azext_iot/central/providers/__init__.py @@ -10,10 +10,12 @@ ) 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/api_token_provider.py b/azext_iot/central/providers/api_token_provider.py new file mode 100644 index 000000000..b76574513 --- /dev/null +++ b/azext_iot/central/providers/api_token_provider.py @@ -0,0 +1,78 @@ +# 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 + + +logger = get_logger(__name__) + + +class CentralApiTokenProvider: + 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, + ): + + 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, + central_dns_suffix=central_dns_suffix, + ) + + def get_api_token_list( + self, central_dns_suffix=CENTRAL_ENDPOINT, + ): + + return central_services.api_token.get_api_token_list( + cmd=self._cmd, + app_id=self._app_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + ) + + def get_api_token( + self, token_id, central_dns_suffix=CENTRAL_ENDPOINT, + ): + return central_services.api_token.get_api_token( + cmd=self._cmd, + app_id=self._app_id, + token_id=token_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + ) + + def delete_api_token( + self, token_id, central_dns_suffix=CENTRAL_ENDPOINT, + ): + return central_services.api_token.delete_api_token( + cmd=self._cmd, + app_id=self._app_id, + token_id=token_id, + token=self._token, + central_dns_suffix=central_dns_suffix, + ) diff --git a/azext_iot/central/services/__init__.py b/azext_iot/central/services/__init__.py index b0c685e94..6463834e2 100644 --- a/azext_iot/central/services/__init__.py +++ b/azext_iot/central/services/__init__.py @@ -4,7 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_iot.central.services import device, device_template, user +from azext_iot.central.services import device, device_template, user, api_token -__all__ = ["device", "device_template", "user"] +__all__ = ["device", "device_template", "user", "api_token"] diff --git a/azext_iot/central/services/api_token.py b/azext_iot/central/services/api_token.py new file mode 100644 index 000000000..fd1a0123d --- /dev/null +++ b/azext_iot/central/services/api_token.py @@ -0,0 +1,125 @@ +# 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 requests + +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 + +logger = get_logger(__name__) + +BASE_PATH = "api/preview/apiTokens" + + +def add_api_token( + cmd, + app_id: str, + token_id: str, + role: Role, + token: str, + central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + Add an API token to a Central app + + Args: + cmd: command passed into az + app_id: name of app (used for forming request URL) + token_id: Unique ID for the API token. + role : permission level to access the application + 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: + token: dict + """ + url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, token_id) + + payload = { + "roles": [{"role": role.value}], + } + + headers = _utility.get_headers(token, cmd, has_json_payload=True) + + response = requests.put(url, headers=headers, json=payload) + return _utility.try_extract_result(response) + + +def get_api_token_list( + cmd, app_id: str, token: str, central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + Get the list of API tokens for a central app. + + 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 ...') + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + tokens: dict + """ + url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) + + headers = _utility.get_headers(token, cmd) + + response = requests.get(url, 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, +): + """ + Get information about a specified API token. + + 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 ...') + token_id: Unique ID for the API token. + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + token: dict + """ + url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, token_id) + + headers = _utility.get_headers(token, cmd) + + response = requests.get(url, headers=headers) + return _utility.try_extract_result(response) + + +def delete_api_token( + cmd, app_id: str, token: str, token_id: str, central_dns_suffix=CENTRAL_ENDPOINT, +): + """ + delete API token from the app. + + 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 ...') + token_id:Unique ID for the API token. + central_dns_suffix: {centralDnsSuffixInPath} as found in docs + + Returns: + result (currently a 201) + """ + url = "https://{}.{}/{}/{}".format(app_id, central_dns_suffix, BASE_PATH, token_id) + + headers = _utility.get_headers(token, cmd) + + response = requests.delete(url, headers=headers) + return _utility.try_extract_result(response) diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index e8db49adc..e269efc52 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -241,6 +241,32 @@ def test_central_user_methods_CRD(self): for user in users: assert user in user_list + def test_central_api_token_methods_CRD(self): + tokens = self._create_api_tokens() + + self.cmd( + "iot central app api-token show --app-id {} --token-id {}".format( + APP_ID, tokens[0].get("id") + ), + ) + + result = self.cmd( + "iot central app api-token list --app-id {}".format(APP_ID,), + ).get_output_in_json() + + token_list = result.get("value") + + for token in tokens: + self._delete_api_token(token.get("id")) + + for token in tokens: + token_info_basic = { + "expiry": token.get("expiry"), + "id": token.get("id"), + "roles": token.get("roles"), + } + assert token_info_basic in token_list + def test_central_device_template_methods_CRD(self): # currently: create, show, list, delete (template_id, template_name) = self._create_device_template() @@ -449,6 +475,31 @@ def _delete_user(self, user_id) -> None: checks=[self.check("result", "success")], ) + def _create_api_tokens(self,): + + tokens = [] + for role in Role: + token_id = self.create_random_name(prefix="aztest", length=24) + command = "iot central app api-token create --app-id {} --token-id {} -r {}".format( + APP_ID, token_id, role.name, + ) + + checks = [ + self.check("id", token_id), + self.check("roles[0].role", role.value), + ] + + tokens.append(self.cmd(command, checks=checks).get_output_in_json()) + return tokens + + def _delete_api_token(self, token_id) -> None: + self.cmd( + "iot central app api-token delete --app-id {} --token-id {}".format( + APP_ID, token_id + ), + checks=[self.check("result", "success")], + ) + def _wait_for_provisioned(self, device_id): command = "iot central app device show --app-id {} -d {}".format( APP_ID, device_id From 7bdf5b5b96b28940f32e59dc5d3e5cf99356cde5 Mon Sep 17 00:00:00 2001 From: Sapan Saxena <31940305+anusapan@users.noreply.github.com> Date: Thu, 6 Aug 2020 13:40:15 -0700 Subject: [PATCH 083/179] Eventhub compatible connection-string command (#214) * New cmd-grp & cmd "iot hub connection-string show" * Fix int test * code refactored * updated command argument * fixed consistent output * Updated test case --- azext_iot/_help.py | 61 +++++++++++++++++++ azext_iot/_params.py | 12 ++++ azext_iot/commands.py | 27 ++++++++- azext_iot/operations/hub.py | 93 ++++++++++++++++++++++++++++- azext_iot/tests/test_iot_ext_int.py | 75 +++++++++++++++++++++++ 5 files changed, 265 insertions(+), 3 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 4e4f3990e..96a55a98b 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -96,6 +96,39 @@ az iot hub monitor-feedback -n {iothub_name} -d {device_id} -w {message_id} """ +helps[ + "iot hub connection-string" +] = """ + type: group + short-summary: Manage IoT Hub connection strings. +""" + +helps[ + "iot hub connection-string show" +] = """ + type: command + short-summary: Show the connection strings for an IoT Hub. + examples: + - name: Show the connection string for all IoT Hubs in a subscription using the default policy and primary key. + text: > + az iot hub connection-string show + - name: Show the connection string for all IoT Hubs in a resource group using the default policy and primary key. + text: > + az iot hub connection-string show --resource-group MyResourceGroup + - name: Show all the connection string of an IoT Hub using primary key. + text: > + az iot hub connection-string show -n MyIotHub --all + - name: Show the connection string of an IoT Hub using default policy and primary key. + text: > + az iot hub connection-string show -n MyIotHub + - name: Show the connection string of an IoT Hub using policy 'service' and secondary key. + text: > + az iot hub connection-string show -n MyIotHub --policy-name service --key-type secondary + - name: Show the eventhub compatible connection string of an IoT Hub\'s default eventhub. + text: > + az iot hub connection-string show -n MyIotHub --default-eventhub +""" + helps[ "iot hub device-identity" ] = """ @@ -190,6 +223,20 @@ short-summary: Show a given IoT Hub device connection string. """ +helps[ + "iot hub device-identity connection-string" +] = """ + type: group + short-summary: Manage IoT device\'s connection string. +""" + +helps[ + "iot hub device-identity connection-string show" +] = """ + type: command + short-summary: Show a given IoT Hub device connection string. +""" + helps[ "iot hub device-identity export" ] = """ @@ -397,6 +444,20 @@ short-summary: Delete a device in an IoT Hub. """ +helps[ + "iot hub module-identity connection-string" +] = """ + type: group + short-summary: Manage IoT device module\'s connection string. +""" + +helps[ + "iot hub module-identity connection-string show" +] = """ + type: command + short-summary: Show a target IoT device module connection string. +""" + helps[ "iot hub module-twin" ] = """ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index b4fd0e477..72570f31d 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -263,6 +263,18 @@ def load_arguments(self, _): help="Twin desired properties.", ) + with self.argument_context("iot hub connection-string") as context: + context.argument( + "show_all", + options_list=["--show-all", "--all"], + help="Allow to show all shared access policies." + ) + context.argument( + "default_eventhub", + options_list=["--default-eventhub", "--eh"], + help="To get eventhub compatible connection-string for the IoT Hub\'s default eventhub." + ) + with self.argument_context("iot hub job") as context: context.argument("job_id", options_list=["--job-id"], help="IoT Hub job Id.") context.argument( diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 404c3f228..328923be0 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -37,7 +37,11 @@ def load_command_table(self, _): "update", getter_name="iot_device_show", setter_name="iot_device_update" ) - cmd_group.command("show-connection-string", "iot_get_device_connection_string") + 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("import", "iot_device_import") cmd_group.command("export", "iot_device_export") cmd_group.command("add-children", "iot_device_children_add") @@ -46,6 +50,11 @@ def load_command_table(self, _): cmd_group.command("get-parent", "iot_device_get_parent") cmd_group.command("set-parent", "iot_device_set_parent") + with self.command_group( + "iot hub device-identity connection-string", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("show", "iot_get_device_connection_string") + with self.command_group( "iot hub module-identity", command_type=iothub_ops ) as cmd_group: @@ -59,7 +68,16 @@ def load_command_table(self, _): setter_name="iot_device_module_update", ) - cmd_group.command("show-connection-string", "iot_get_module_connection_string") + cmd_group.command( + "show-connection-string", + "iot_get_module_connection_string", + deprecate_info=self.deprecate(redirect='az iot hub module-identity connection-string show') + ) + + with self.command_group( + "iot hub module-identity connection-string", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("show", "iot_get_module_connection_string") with self.command_group( "iot hub module-twin", command_type=iothub_ops @@ -107,6 +125,11 @@ def load_command_table(self, _): cmd_group.command("show", "iot_hub_distributed_tracing_show") cmd_group.command("update", "iot_hub_distributed_tracing_update") + with self.command_group( + "iot hub connection-string", command_type=iothub_ops + ) as cmd_group: + cmd_group.command("show", "iot_hub_connection_string_show") + with self.command_group("iot edge", command_type=iothub_ops) as cmd_group: cmd_group.command("set-modules", "iot_edge_set_modules") diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 113ac5f49..d99578d23 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -17,7 +17,7 @@ TRACING_ALLOWED_FOR_SKU, ) from azext_iot.common.sas_token_auth import SasTokenAuthentication -from azext_iot.common.shared import DeviceAuthType, SdkType, ProtocolType, ConfigType +from azext_iot.common.shared import DeviceAuthType, SdkType, ProtocolType, ConfigType, KeyType from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot.common.utility import ( shell_safe_json_parse, @@ -2434,6 +2434,97 @@ def iot_hub_distributed_tracing_update( ) +def iot_hub_connection_string_show( + cmd, + hub_name=None, + resource_group_name=None, + policy_name='iothubowner', + key_type=KeyType.primary.value, + show_all=False, + default_eventhub=False +): + discovery = IotHubDiscovery(cmd) + + if hub_name is None: + hubs = discovery.get_iothubs(resource_group_name) + if hubs is None: + raise CLIError("No IoT Hub found.") + + def conn_str_getter(hub): + return _get_hub_connection_string( + discovery, + hub, + policy_name, + key_type, + show_all, + default_eventhub + ) + return [{'name': hub.name, 'connectionString': conn_str_getter(hub) + if show_all else conn_str_getter(hub)[0]} for hub in hubs] + hub = discovery.find_iothub(hub_name, resource_group_name) + if hub: + conn_str = _get_hub_connection_string( + discovery, + hub, + policy_name, + key_type, + show_all, + default_eventhub + ) + return {'connectionString': conn_str if show_all else conn_str[0]} + + +def _get_hub_connection_string( + discovery, + hub, + policy_name, + key_type, + show_all, + default_eventhub +): + + policies = [] + if show_all: + policies.extend( + discovery.get_policies( + hub.name, + hub.additional_properties['resourcegroup'] + ) + ) + else: + policies.append( + discovery.find_policy( + hub.name, + hub.additional_properties['resourcegroup'], + policy_name + ) + ) + + if default_eventhub: + cs_template_eventhub = 'Endpoint={};SharedAccessKeyName={};SharedAccessKey={};EntityPath={}' + endpoint = hub.properties.event_hub_endpoints['events'].endpoint + entityPath = hub.properties.event_hub_endpoints['events'].path + return [ + cs_template_eventhub.format( + endpoint, + p.key_name, + p.secondary_key if key_type == KeyType.secondary.value else p.primary_key, + entityPath + ) for p in policies if "serviceconnect" in p.rights.value.lower() + ] + + hostname = hub.properties.host_name + cs_template = 'HostName={};SharedAccessKeyName={};SharedAccessKey={}' + return [ + cs_template.format( + hostname, + p.key_name, + p.secondary_key + if key_type == KeyType.secondary.value else p.primary_key + ) for p in policies + ] + + def _iot_hub_monitor_feedback(target, device_id, wait_on_id): from azext_iot.monitor import event diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 5ce4a1743..e29168061 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -71,6 +71,38 @@ def test_hub(self): 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://' + + 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} --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( @@ -436,6 +468,27 @@ def test_hub_devices(self): 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] @@ -960,6 +1013,28 @@ def test_hub_modules(self): 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 From ca853a5e67a48ce8a50efa596d830a641cf9aa9e Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 7 Aug 2020 14:08:20 -0700 Subject: [PATCH 084/179] Removal of _bind_sdk and related components + misc improvements. (#227) --- azext_iot/_factory.py | 66 +- .../central/providers/devicetwin_provider.py | 8 +- azext_iot/central/services/device.py | 2 +- azext_iot/monitor/handlers/central_handler.py | 2 +- azext_iot/monitor/parsers/issue.py | 2 +- azext_iot/operations/dps.py | 115 +- azext_iot/operations/hub.py | 106 +- .../operations/digital_twins_operations.py | 2 +- azext_iot/sdk/service/__init__.py | 18 - .../service/iot_hub_gateway_service_apis.py | 2738 ----------------- azext_iot/sdk/service/models/__init__.py | 92 - .../models/authentication_mechanism.py | 37 - .../models/bulk_registry_operation_result.py | 38 - .../service/models/cloud_to_device_method.py | 50 - .../models/cloud_to_device_method_result.py | 32 - azext_iot/sdk/service/models/configuration.py | 72 - .../service/models/configuration_content.py | 36 - .../service/models/configuration_metrics.py | 32 - .../configuration_queries_test_input.py | 32 - .../configuration_queries_test_response.py | 32 - azext_iot/sdk/service/models/desired.py | 29 - azext_iot/sdk/service/models/desired_state.py | 40 - azext_iot/sdk/service/models/device.py | 77 - .../sdk/service/models/device_capabilities.py | 28 - .../service/models/device_job_statistics.py | 44 - .../models/device_registry_operation_error.py | 119 - .../device_registry_operation_warning.py | 37 - .../service/models/digital_twin_interfaces.py | 32 - .../models/digital_twin_interfaces_patch.py | 28 - ..._twin_interfaces_patch_interfaces_value.py | 28 - ...patch_interfaces_value_properties_value.py | 28 - ...terfaces_value_properties_value_desired.py | 29 - .../service/models/export_import_device.py | 70 - azext_iot/sdk/service/models/interface.py | 32 - .../sdk/service/models/job_properties.py | 89 - azext_iot/sdk/service/models/job_request.py | 62 - azext_iot/sdk/service/models/job_response.py | 87 - azext_iot/sdk/service/models/module.py | 65 - azext_iot/sdk/service/models/property.py | 32 - .../sdk/service/models/property_container.py | 39 - .../models/purge_message_queue_result.py | 36 - azext_iot/sdk/service/models/query_result.py | 38 - .../sdk/service/models/query_specification.py | 28 - .../sdk/service/models/registry_statistics.py | 36 - azext_iot/sdk/service/models/reported.py | 32 - .../sdk/service/models/service_statistics.py | 28 - azext_iot/sdk/service/models/symmetric_key.py | 32 - azext_iot/sdk/service/models/twin.py | 107 - .../sdk/service/models/twin_properties.py | 39 - .../sdk/service/models/x509_thumbprint.py | 32 - azext_iot/sdk/service/version.py | 12 - .../configurations/test_iot_config_unit.py | 3 - azext_iot/tests/test_iot_central_unit.py | 39 +- azext_iot/tests/test_iot_ext_int.py | 4 +- azext_iot/tests/test_iot_utility_unit.py | 6 +- 55 files changed, 180 insertions(+), 4799 deletions(-) delete mode 100644 azext_iot/sdk/service/__init__.py delete mode 100644 azext_iot/sdk/service/iot_hub_gateway_service_apis.py delete mode 100644 azext_iot/sdk/service/models/__init__.py delete mode 100644 azext_iot/sdk/service/models/authentication_mechanism.py delete mode 100644 azext_iot/sdk/service/models/bulk_registry_operation_result.py delete mode 100644 azext_iot/sdk/service/models/cloud_to_device_method.py delete mode 100644 azext_iot/sdk/service/models/cloud_to_device_method_result.py delete mode 100644 azext_iot/sdk/service/models/configuration.py delete mode 100644 azext_iot/sdk/service/models/configuration_content.py delete mode 100644 azext_iot/sdk/service/models/configuration_metrics.py delete mode 100644 azext_iot/sdk/service/models/configuration_queries_test_input.py delete mode 100644 azext_iot/sdk/service/models/configuration_queries_test_response.py delete mode 100644 azext_iot/sdk/service/models/desired.py delete mode 100644 azext_iot/sdk/service/models/desired_state.py delete mode 100644 azext_iot/sdk/service/models/device.py delete mode 100644 azext_iot/sdk/service/models/device_capabilities.py delete mode 100644 azext_iot/sdk/service/models/device_job_statistics.py delete mode 100644 azext_iot/sdk/service/models/device_registry_operation_error.py delete mode 100644 azext_iot/sdk/service/models/device_registry_operation_warning.py delete mode 100644 azext_iot/sdk/service/models/digital_twin_interfaces.py delete mode 100644 azext_iot/sdk/service/models/digital_twin_interfaces_patch.py delete mode 100644 azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value.py delete mode 100644 azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py delete mode 100644 azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py delete mode 100644 azext_iot/sdk/service/models/export_import_device.py delete mode 100644 azext_iot/sdk/service/models/interface.py delete mode 100644 azext_iot/sdk/service/models/job_properties.py delete mode 100644 azext_iot/sdk/service/models/job_request.py delete mode 100644 azext_iot/sdk/service/models/job_response.py delete mode 100644 azext_iot/sdk/service/models/module.py delete mode 100644 azext_iot/sdk/service/models/property.py delete mode 100644 azext_iot/sdk/service/models/property_container.py delete mode 100644 azext_iot/sdk/service/models/purge_message_queue_result.py delete mode 100644 azext_iot/sdk/service/models/query_result.py delete mode 100644 azext_iot/sdk/service/models/query_specification.py delete mode 100644 azext_iot/sdk/service/models/registry_statistics.py delete mode 100644 azext_iot/sdk/service/models/reported.py delete mode 100644 azext_iot/sdk/service/models/service_statistics.py delete mode 100644 azext_iot/sdk/service/models/symmetric_key.py delete mode 100644 azext_iot/sdk/service/models/twin.py delete mode 100644 azext_iot/sdk/service/models/twin_properties.py delete mode 100644 azext_iot/sdk/service/models/x509_thumbprint.py delete mode 100644 azext_iot/sdk/service/version.py diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py index 5b9f13849..286eb0b63 100644 --- a/azext_iot/_factory.py +++ b/azext_iot/_factory.py @@ -13,12 +13,10 @@ from msrestazure.azure_exceptions import CloudError __all__ = [ + "SdkResolver", "CloudError", "iot_hub_service_factory", "iot_service_provisioning_factory", - "SdkResolver", - "_bind_sdk", - "_get_sdk_exception_type", ] @@ -59,9 +57,10 @@ def iot_service_provisioning_factory(cli_ctx, *_): class SdkResolver(object): - def __init__(self, target, device_id=None): + def __init__(self, target, device_id=None, auth_override=None): self.target = target self.device_id = device_id + self.auth_override = auth_override # This initialization will likely need to change to support more variation of SDK self.sas_uri = self.target["entity"] @@ -78,6 +77,7 @@ def _construct_sdk_map(self): SdkType.service_sdk: self._get_iothub_service_sdk, # Don't need to call here SdkType.device_sdk: self._get_iothub_device_sdk, SdkType.pnp_sdk: self._get_pnp_runtime_sdk, + SdkType.dps_sdk: self._get_dps_service_sdk, } def _get_iothub_device_sdk(self): @@ -94,10 +94,14 @@ def _get_iothub_device_sdk(self): def _get_iothub_service_sdk(self): from azext_iot.sdk.iothub.service import IotHubGatewayServiceAPIs - credentials = SasTokenAuthentication( - uri=self.sas_uri, - shared_access_policy_name=self.target["policy"], - shared_access_key=self.target["primarykey"], + credentials = ( + self.auth_override + if self.auth_override + else 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) @@ -113,45 +117,15 @@ def _get_pnp_runtime_sdk(self): return IotHubGatewayServiceAPIs(credentials=credentials, base_url=self.endpoint) + def _get_dps_service_sdk(self): + from azext_iot.sdk.dps import ProvisioningServiceClient -# TODO: Deprecated. To be removed asap. -def _bind_sdk(target, sdk_type, device_id=None, auth=None): - from azext_iot.sdk.service.iot_hub_gateway_service_apis import ( - IotHubGatewayServiceAPIs, - ) - from azext_iot.sdk.dps import ProvisioningServiceClient - - sas_uri = target["entity"] - endpoint = "https://{}".format(sas_uri) - if device_id: - sas_uri = "{}/devices/{}".format(sas_uri, device_id) - - if not auth: - auth = SasTokenAuthentication(sas_uri, target["policy"], target["primarykey"]) - - if sdk_type is SdkType.service_sdk: - return ( - IotHubGatewayServiceAPIs(auth, endpoint), - _get_sdk_exception_type(sdk_type), + credentials = SasTokenAuthentication( + uri=self.sas_uri, + shared_access_policy_name=self.target["policy"], + shared_access_key=self.target["primarykey"], ) - if sdk_type is SdkType.dps_sdk: - return ( - ProvisioningServiceClient(auth, endpoint), - _get_sdk_exception_type(sdk_type), + return ProvisioningServiceClient( + credentials=credentials, base_url=self.endpoint ) - - return None - - -# TODO: Dependency for _bind_sdk. Will be removed asap. -def _get_sdk_exception_type(sdk_type): - from importlib import import_module - - exception_library = { - SdkType.service_sdk: import_module("msrestazure.azure_exceptions"), - SdkType.dps_sdk: import_module( - "azext_iot.sdk.dps.models.provisioning_service_error_details" - ), - } - return exception_library.get(sdk_type, None) diff --git a/azext_iot/central/providers/devicetwin_provider.py b/azext_iot/central/providers/devicetwin_provider.py index 51510f7ea..a44b3a6a9 100644 --- a/azext_iot/central/providers/devicetwin_provider.py +++ b/azext_iot/central/providers/devicetwin_provider.py @@ -7,7 +7,7 @@ from knack.util import CLIError from knack.log import get_logger from azext_iot.common.utility import find_between -from azext_iot._factory import _bind_sdk +from azext_iot._factory import SdkResolver, CloudError from azext_iot.common.shared import SdkType from azext_iot.common.utility import unpack_msrest_error from azext_iot.common.sas_token_auth import BasicSasTokenAuthentication @@ -50,10 +50,10 @@ 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, errors = _bind_sdk(target, SdkType.service_sdk, auth=auth) + service_sdk = SdkResolver(target=target, auth_override=auth).get_sdk(SdkType.service_sdk) try: - return service_sdk.get_twin(self._device_id) - except errors.CloudError as e: + return service_sdk.twin.get_device_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/services/device.py b/azext_iot/central/services/device.py index 1d9506bec..82ac30da7 100644 --- a/azext_iot/central/services/device.py +++ b/azext_iot/central/services/device.py @@ -106,7 +106,7 @@ def get_device_registration_summary( url = "https://{}.{}/{}".format(app_id, central_dns_suffix, BASE_PATH) headers = _utility.get_headers(token, cmd) - logger.warn( + logger.warning( "This command may take a long time to complete if your app contains a lot of devices" ) while url: diff --git a/azext_iot/monitor/handlers/central_handler.py b/azext_iot/monitor/handlers/central_handler.py index 210c896b6..54311ff1c 100644 --- a/azext_iot/monitor/handlers/central_handler.py +++ b/azext_iot/monitor/handlers/central_handler.py @@ -96,7 +96,7 @@ def generate_startup_string(self, name: str): filter_text = ".\nFiltering on device: {}".format(device_id) if module_id: - logger.warn("Module filtering is applicable only for edge devices.") + logger.warning("Module filtering is applicable only for edge devices.") filter_text += ".\nFiltering on module: {}".format(module_id) exit_text = "" diff --git a/azext_iot/monitor/parsers/issue.py b/azext_iot/monitor/parsers/issue.py index 72a29be55..266b4203c 100644 --- a/azext_iot/monitor/parsers/issue.py +++ b/azext_iot/monitor/parsers/issue.py @@ -34,7 +34,7 @@ def _log(self, to_log: str): logger.info(to_log) if self.severity == Severity.warning: - logger.warn(to_log) + logger.warning(to_log) if self.severity == Severity.error: logger.error(to_log) diff --git a/azext_iot/operations/dps.py b/azext_iot/operations/dps.py index d93cc7b70..4c16f5f33 100644 --- a/azext_iot/operations/dps.py +++ b/azext_iot/operations/dps.py @@ -14,7 +14,7 @@ from azext_iot.common.utility import shell_safe_json_parse from azext_iot.common.certops import open_certificate from azext_iot.operations.generic import _execute_query -from azext_iot._factory import _bind_sdk +from azext_iot._factory import SdkResolver from azext_iot.sdk.dps.models.individual_enrollment import IndividualEnrollment from azext_iot.sdk.dps.models.custom_allocation_definition import CustomAllocationDefinition from azext_iot.sdk.dps.models.attestation_mechanism import AttestationMechanism @@ -30,6 +30,7 @@ from azext_iot.sdk.dps.models.x509_ca_references import X509CAReferences from azext_iot.sdk.dps.models.reprovision_policy import ReprovisionPolicy from azext_iot.sdk.dps.models import DeviceCapabilities +from azext_iot.sdk.dps.models import ProvisioningServiceErrorDetailsException # TODO: Regen SDK logger = get_logger(__name__) @@ -39,22 +40,26 @@ def iot_dps_device_enrollment_list(client, dps_name, resource_group_name, top=None): from azext_iot.sdk.dps.models.query_specification import QuerySpecification target = get_iot_dps_connection_string(client, dps_name, resource_group_name) + try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) query_command = "SELECT *" query = [QuerySpecification(query_command)] - return _execute_query(query, m_sdk.device_enrollment.query, top) - except errors.ProvisioningServiceErrorDetailsException as e: + return _execute_query(query, sdk.device_enrollment.query, top) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) def iot_dps_device_enrollment_get(client, enrollment_id, dps_name, resource_group_name): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) - return m_sdk.device_enrollment.get(enrollment_id) - except errors.ProvisioningServiceErrorDetailsException as e: + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + + return sdk.device_enrollment.get(enrollment_id) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -81,7 +86,9 @@ def iot_dps_device_enrollment_create(client, api_version=None): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + if attestation_type == AttestationType.tpm.value: if not endorsement_key: raise CLIError('Endorsement key is requried') @@ -120,8 +127,8 @@ def iot_dps_device_enrollment_create(client, allocation_policy, iot_hub_list, custom_allocation_definition) - return m_sdk.device_enrollment.create_or_update(enrollment_id, enrollment) - except errors.ProvisioningServiceErrorDetailsException as e: + return sdk.device_enrollment.create_or_update(enrollment_id, enrollment) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -150,8 +157,10 @@ def iot_dps_device_enrollment_update(client, api_version=None): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) - enrollment_record = m_sdk.device_enrollment.get(enrollment_id) + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + + enrollment_record = sdk.device_enrollment.get(enrollment_id) # Verify etag if etag and hasattr(enrollment_record, 'etag') and etag != enrollment_record.etag.replace('"', ''): raise LookupError("enrollment etag doesn't match.") @@ -177,7 +186,7 @@ def iot_dps_device_enrollment_update(client, remove_certificate, remove_secondary_certificate) else: - enrollment_record.attestation = m_sdk.device_enrollment.attestation_mechanism_method(enrollment_id) + enrollment_record.attestation = sdk.device_enrollment.attestation_mechanism_method(enrollment_id) if primary_key: enrollment_record.attestation.symmetric_key.primary_key = primary_key if secondary_key: @@ -212,17 +221,19 @@ def iot_dps_device_enrollment_update(client, if edge_enabled is not None: enrollment_record.capabilities = DeviceCapabilities(iot_edge=edge_enabled) - return m_sdk.device_enrollment.create_or_update(enrollment_id, enrollment_record, etag) - except errors.ProvisioningServiceErrorDetailsException as e: + return sdk.device_enrollment.create_or_update(enrollment_id, enrollment_record, etag) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) def iot_dps_device_enrollment_delete(client, enrollment_id, dps_name, resource_group_name): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) - return m_sdk.device_enrollment.delete(enrollment_id) - except errors.ProvisioningServiceErrorDetailsException as e: + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + + return sdk.device_enrollment.delete(enrollment_id) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -230,23 +241,27 @@ def iot_dps_device_enrollment_delete(client, enrollment_id, dps_name, resource_g def iot_dps_device_enrollment_group_list(client, dps_name, resource_group_name, top=None): from azext_iot.sdk.dps.models.query_specification import QuerySpecification + target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) query_command = "SELECT *" query1 = [QuerySpecification(query_command)] - return _execute_query(query1, m_sdk.device_enrollment_group.query, top) - except errors.ProvisioningServiceErrorDetailsException as e: + return _execute_query(query1, sdk.device_enrollment_group.query, top) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) def iot_dps_device_enrollment_group_get(client, enrollment_id, dps_name, resource_group_name): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) - return m_sdk.device_enrollment_group.get(enrollment_id) - except errors.ProvisioningServiceErrorDetailsException as e: + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + + return sdk.device_enrollment_group.get(enrollment_id) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -272,7 +287,9 @@ def iot_dps_device_enrollment_group_create(client, api_version=None): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + if not certificate_path and not secondary_certificate_path: if not root_ca_name and not secondary_root_ca_name: attestation = AttestationMechanism(AttestationType.symmetricKey.value, @@ -314,8 +331,8 @@ def iot_dps_device_enrollment_group_create(client, allocation_policy, iot_hub_list, custom_allocation_definition) - return m_sdk.device_enrollment_group.create_or_update(enrollment_id, group_enrollment) - except errors.ProvisioningServiceErrorDetailsException as e: + return sdk.device_enrollment_group.create_or_update(enrollment_id, group_enrollment) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -344,8 +361,10 @@ def iot_dps_device_enrollment_group_update(client, api_version=None): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) - enrollment_record = m_sdk.device_enrollment_group.get(enrollment_id) + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + + enrollment_record = sdk.device_enrollment_group.get(enrollment_id) # Verify etag if etag and hasattr(enrollment_record, 'etag') and etag != enrollment_record.etag.replace('"', ''): raise LookupError("enrollment etag doesn't match.") @@ -353,7 +372,7 @@ def iot_dps_device_enrollment_group_update(client, etag = enrollment_record.etag.replace('"', '') # Update enrollment information if enrollment_record.attestation.type == AttestationType.symmetricKey.value: - enrollment_record.attestation = m_sdk.device_enrollment_group.attestation_mechanism_method(enrollment_id) + enrollment_record.attestation = sdk.device_enrollment_group.attestation_mechanism_method(enrollment_id) if primary_key: enrollment_record.attestation.symmetric_key.primary_key = primary_key if secondary_key: @@ -413,17 +432,19 @@ def iot_dps_device_enrollment_group_update(client, enrollment_record.custom_allocation_definition = CustomAllocationDefinition(webhook_url, api_version) if edge_enabled is not None: enrollment_record.capabilities = DeviceCapabilities(iot_edge=edge_enabled) - return m_sdk.device_enrollment_group.create_or_update(enrollment_id, enrollment_record, etag) - except errors.ProvisioningServiceErrorDetailsException as e: + return sdk.device_enrollment_group.create_or_update(enrollment_id, enrollment_record, etag) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) def iot_dps_device_enrollment_group_delete(client, enrollment_id, dps_name, resource_group_name): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) - return m_sdk.device_enrollment_group.delete(enrollment_id) - except errors.ProvisioningServiceErrorDetailsException as e: + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + + return sdk.device_enrollment_group.delete(enrollment_id) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -432,27 +453,33 @@ def iot_dps_device_enrollment_group_delete(client, enrollment_id, dps_name, reso def iot_dps_registration_list(client, dps_name, resource_group_name, enrollment_id): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) - return m_sdk.registration_state.query_registration_state(enrollment_id) - except errors.ProvisioningServiceErrorDetailsException as e: + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + + return sdk.registration_state.query_registration_state(enrollment_id) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) def iot_dps_registration_get(client, dps_name, resource_group_name, registration_id): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) - return m_sdk.registration_state.get_registration_state(registration_id) - except errors.ProvisioningServiceErrorDetailsException as e: + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + + return sdk.registration_state.get_registration_state(registration_id) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) def iot_dps_registration_delete(client, dps_name, resource_group_name, registration_id): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: - m_sdk, errors = _bind_sdk(target, SdkType.dps_sdk) - return m_sdk.registration_state.delete_registration_state(registration_id) - except errors.ProvisioningServiceErrorDetailsException as e: + resolver = SdkResolver(target=target) + sdk = resolver.get_sdk(SdkType.dps_sdk) + + return sdk.registration_state.delete_registration_state(registration_id) + except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index d99578d23..ab3a988e9 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -17,7 +17,13 @@ TRACING_ALLOWED_FOR_SKU, ) from azext_iot.common.sas_token_auth import SasTokenAuthentication -from azext_iot.common.shared import DeviceAuthType, SdkType, ProtocolType, ConfigType, KeyType +from azext_iot.common.shared import ( + DeviceAuthType, + SdkType, + ProtocolType, + ConfigType, + KeyType, +) from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot.common.utility import ( shell_safe_json_parse, @@ -910,7 +916,7 @@ def _iot_hub_configuration_create( resource_group_name=None, login=None, ): - from azext_iot.sdk.service.models import ( + from azext_iot.sdk.iothub.service.models import ( Configuration, ConfigurationContent, ConfigurationMetrics, @@ -965,8 +971,8 @@ def _iot_hub_configuration_create( target_condition=target_condition, etag="*", priority=priority, - content_type="assignment", ) + try: return service_sdk.configuration.create_or_update( id=config_id, configuration=config @@ -1061,7 +1067,7 @@ def _validate_payload_schema(content): def iot_hub_configuration_update( cmd, config_id, parameters, hub_name=None, resource_group_name=None, login=None ): - from azext_iot.sdk.service.models.configuration import Configuration + from azext_iot.sdk.iothub.service.models import Configuration from azext_iot.common.utility import verify_transform discovery = IotHubDiscovery(cmd) @@ -1375,7 +1381,7 @@ def iot_device_method( resource_group_name=None, login=None, ): - from azext_iot.sdk.service.models import CloudToDeviceMethod + from azext_iot.sdk.iothub.service.models import CloudToDeviceMethod from azext_iot.constants import ( METHOD_INVOKE_MAX_TIMEOUT_SEC, METHOD_INVOKE_MIN_TIMEOUT_SEC, @@ -1405,7 +1411,12 @@ def iot_device_method( method_payload, argument_name="method-payload" ) - method = CloudToDeviceMethod(method_name, timeout, timeout, method_payload) + method = CloudToDeviceMethod( + method_name=method_name, + response_timeout_in_seconds=timeout, + connect_timeout_in_seconds=timeout, + payload=method_payload, + ) return service_sdk.device_method.invoke_device_method( device_id=device_id, direct_method_request=method, timeout=timeout ) @@ -1427,7 +1438,7 @@ def iot_device_module_method( resource_group_name=None, login=None, ): - from azext_iot.sdk.service.models.cloud_to_device_method import CloudToDeviceMethod + from azext_iot.sdk.iothub.service.models import CloudToDeviceMethod from azext_iot.constants import ( METHOD_INVOKE_MAX_TIMEOUT_SEC, METHOD_INVOKE_MIN_TIMEOUT_SEC, @@ -1457,7 +1468,12 @@ def iot_device_module_method( method_payload, argument_name="method-payload" ) - method = CloudToDeviceMethod(method_name, timeout, timeout, method_payload) + method = CloudToDeviceMethod( + method_name=method_name, + response_timeout_in_seconds=timeout, + connect_timeout_in_seconds=timeout, + payload=method_payload, + ) return service_sdk.device_method.invoke_module_method( device_id=device_id, module_id=module_id, @@ -2438,10 +2454,10 @@ def iot_hub_connection_string_show( cmd, hub_name=None, resource_group_name=None, - policy_name='iothubowner', + policy_name="iothubowner", key_type=KeyType.primary.value, show_all=False, - default_eventhub=False + default_eventhub=False, ): discovery = IotHubDiscovery(cmd) @@ -2452,76 +2468,70 @@ def iot_hub_connection_string_show( def conn_str_getter(hub): return _get_hub_connection_string( - discovery, - hub, - policy_name, - key_type, - show_all, - default_eventhub + discovery, hub, policy_name, key_type, show_all, default_eventhub ) - return [{'name': hub.name, 'connectionString': conn_str_getter(hub) - if show_all else conn_str_getter(hub)[0]} for hub in hubs] + + return [ + { + "name": hub.name, + "connectionString": conn_str_getter(hub) + if show_all + else conn_str_getter(hub)[0], + } + for hub in hubs + ] hub = discovery.find_iothub(hub_name, resource_group_name) if hub: conn_str = _get_hub_connection_string( - discovery, - hub, - policy_name, - key_type, - show_all, - default_eventhub + discovery, hub, policy_name, key_type, show_all, default_eventhub ) - return {'connectionString': conn_str if show_all else conn_str[0]} + return {"connectionString": conn_str if show_all else conn_str[0]} def _get_hub_connection_string( - discovery, - hub, - policy_name, - key_type, - show_all, - default_eventhub + discovery, hub, policy_name, key_type, show_all, default_eventhub ): policies = [] if show_all: policies.extend( - discovery.get_policies( - hub.name, - hub.additional_properties['resourcegroup'] - ) + discovery.get_policies(hub.name, hub.additional_properties["resourcegroup"]) ) else: policies.append( discovery.find_policy( - hub.name, - hub.additional_properties['resourcegroup'], - policy_name + hub.name, hub.additional_properties["resourcegroup"], policy_name ) ) if default_eventhub: - cs_template_eventhub = 'Endpoint={};SharedAccessKeyName={};SharedAccessKey={};EntityPath={}' - endpoint = hub.properties.event_hub_endpoints['events'].endpoint - entityPath = hub.properties.event_hub_endpoints['events'].path + cs_template_eventhub = ( + "Endpoint={};SharedAccessKeyName={};SharedAccessKey={};EntityPath={}" + ) + endpoint = hub.properties.event_hub_endpoints["events"].endpoint + entityPath = hub.properties.event_hub_endpoints["events"].path return [ cs_template_eventhub.format( endpoint, p.key_name, - p.secondary_key if key_type == KeyType.secondary.value else p.primary_key, - entityPath - ) for p in policies if "serviceconnect" in p.rights.value.lower() + p.secondary_key + if key_type == KeyType.secondary.value + else p.primary_key, + entityPath, + ) + for p in policies + if "serviceconnect" in p.rights.value.lower() ] hostname = hub.properties.host_name - cs_template = 'HostName={};SharedAccessKeyName={};SharedAccessKey={}' + cs_template = "HostName={};SharedAccessKeyName={};SharedAccessKey={}" return [ cs_template.format( hostname, p.key_name, - p.secondary_key - if key_type == KeyType.secondary.value else p.primary_key - ) for p in policies + p.secondary_key if key_type == KeyType.secondary.value else p.primary_key, + ) + for p in policies ] diff --git a/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py b/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py index d7685dbe7..a9a040cde 100644 --- a/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py +++ b/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py @@ -109,7 +109,7 @@ def _create_or_update_initial( path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), - 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=1) } url = self._client.format_url(url, **path_format_arguments) diff --git a/azext_iot/sdk/service/__init__.py b/azext_iot/sdk/service/__init__.py deleted file mode 100644 index 8c7711c56..000000000 --- a/azext_iot/sdk/service/__init__.py +++ /dev/null @@ -1,18 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator 2.3.33.0 -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .iot_hub_gateway_service_apis import IotHubGatewayServiceAPIs -from .version import VERSION - -__all__ = ['IotHubGatewayServiceAPIs'] - -__version__ = VERSION - diff --git a/azext_iot/sdk/service/iot_hub_gateway_service_apis.py b/azext_iot/sdk/service/iot_hub_gateway_service_apis.py deleted file mode 100644 index 0c29ba0ff..000000000 --- a/azext_iot/sdk/service/iot_hub_gateway_service_apis.py +++ /dev/null @@ -1,2738 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator 2.3.33.0 -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.service_client import ServiceClient -from msrest import Serializer, Deserializer -from msrestazure import AzureConfiguration -from msrest.pipeline import ClientRawResponse -from msrestazure.azure_exceptions import CloudError -import uuid -from . import models -from azext_iot.constants import USER_AGENT -from azext_iot.constants import BASE_API_VERSION, PNP_API_VERSION - - -class IotHubGatewayServiceAPIsConfiguration(AzureConfiguration): - """Configuration for IotHubGatewayServiceAPIs - Note that all parameters used to create this instance are saved as instance - attributes. - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - if credentials is None: - raise ValueError("Parameter 'credentials' must not be None.") - if not base_url: - base_url = 'https://localhost' - - super(IotHubGatewayServiceAPIsConfiguration, self).__init__(base_url) - self.add_user_agent('iothubgatewayserviceapi/{}'.format(BASE_API_VERSION)) - self.add_user_agent(USER_AGENT) - - self.credentials = credentials - - -class IotHubGatewayServiceAPIs(object): - """IotHubGatewayServiceAPIs - - :ivar config: Configuration for client. - :vartype config: IotHubGatewayServiceAPIs - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - self.config = IotHubGatewayServiceAPIsConfiguration(credentials, base_url) - self._client = ServiceClient(self.config.credentials, self.config) - - client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = BASE_API_VERSION - self._serialize = Serializer(client_models) - self._deserialize = Deserializer(client_models) - - - def get_configuration( - self, id, custom_headers=None, raw=False, **operation_config): - """Retrieve a configuration for Iot Hub devices and modules by it - identifier. - - :param id: - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Configuration or ClientRawResponse if raw=true - :rtype: ~service.models.Configuration or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_configuration.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - # @digimaun - deserialize as {object} from Configuration - if response.status_code == 200: - deserialized = self._deserialize('{object}', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_configuration.metadata = {'url': '/configurations/{id}'} - - def create_or_update_configuration( - self, id, configuration, if_match=None, custom_headers=None, raw=False, **operation_config): - """Create or update the configuration for devices or modules of an IoT - hub. An ETag must not be specified for the create operation. An ETag - must be specified for the update operation. Note that configuration Id - and Content cannot be updated by the user. - - :param id: - :type id: str - :param configuration: - :type configuration: ~service.models.Configuration - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Configuration or ClientRawResponse if raw=true - :rtype: ~service.models.Configuration or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.create_or_update_configuration.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(configuration, 'Configuration') - - # Construct and send request - request = self._client.put(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200, 201]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Configuration', response) - if response.status_code == 201: - deserialized = self._deserialize('Configuration', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_or_update_configuration.metadata = {'url': '/configurations/{id}'} - - def delete_configuration( - self, id, if_match=None, custom_headers=None, raw=False, **operation_config): - """Delete the configuration for devices or modules of an IoT hub. This - request requires the If-Match header. The client may specify the ETag - for the device identity on the request in order to compare to the ETag - maintained by the service for the purpose of optimistic concurrency. - The delete operation is performed only if the ETag sent by the client - matches the value maintained by the server, indicating that the device - identity has not been modified since it was retrieved by the client. To - force an unconditional delete, set If-Match to the wildcard character - (*). - - :param id: - :type id: str - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.delete_configuration.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [204]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - delete_configuration.metadata = {'url': '/configurations/{id}'} - - def get_configurations( - self, top=None, custom_headers=None, raw=False, **operation_config): - """Get multiple configurations for devices or modules of an IoT Hub. - Returns the specified number of configurations for Iot Hub. Pagination - is not supported. - - :param top: - :type top: int - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~service.models.Configuration] or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_configurations.metadata['url'] - - # Construct parameters - query_parameters = {} - if top is not None: - query_parameters['top'] = self._serialize.query("top", top, 'int') - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[object]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_configurations.metadata = {'url': '/configurations'} - - def test_configuration_queries( - self, input, custom_headers=None, raw=False, **operation_config): - """ - - :param input: - :type input: ~service.models.ConfigurationQueriesTestInput - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: ConfigurationQueriesTestResponse or ClientRawResponse if - raw=true - :rtype: ~service.models.ConfigurationQueriesTestResponse or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.test_configuration_queries.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(input, 'ConfigurationQueriesTestInput') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - # @digimaun - ConfigurationQueriesTestResponse - deserialized = self._deserialize('object', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - test_configuration_queries.metadata = {'url': '/configurations/testQueries'} - - def get_device_registry_statistics( - self, custom_headers=None, raw=False, **operation_config): - """Retrieves statistics about device identities in the IoT hub’s identity - registry. - - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: RegistryStatistics or ClientRawResponse if raw=true - :rtype: ~service.models.RegistryStatistics or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_device_registry_statistics.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('RegistryStatistics', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_device_registry_statistics.metadata = {'url': '/statistics/devices'} - - def get_service_statistics( - self, custom_headers=None, raw=False, **operation_config): - """Retrieves service statistics for this IoT hub’s identity registry. - - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: ServiceStatistics or ClientRawResponse if raw=true - :rtype: ~service.models.ServiceStatistics or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_service_statistics.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('ServiceStatistics', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_service_statistics.metadata = {'url': '/statistics/service'} - - def get_devices( - self, top=None, custom_headers=None, raw=False, **operation_config): - """Get the identities of multiple devices from the IoT hub identity - registry. Not recommended. Use the IoT Hub query language to retrieve - device twin and device identity information. See - https://docs.microsoft.com/en-us/rest/api/iothub/deviceapi/querydevices. - - :param top: This parameter when specified, defines the maximum number - of device identities that are returned. Any value outside the range of - 1-1000 is considered to be 1000. - :type top: int - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~service.models.Device] or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_devices.metadata['url'] - - # Construct parameters - query_parameters = {} - if top is not None: - query_parameters['top'] = self._serialize.query("top", top, 'int') - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[Device]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_devices.metadata = {'url': '/devices'} - - def bulk_create_or_update_devices( - self, devices, custom_headers=None, raw=False, **operation_config): - """Create, update, or delete the identities of multiple devices from the - IoT hub identity registry. - - Create, update, or delete the identiies of multiple devices from the - IoT hub identity registry. A device identity can be specified only once - in the list. Different operations (create, update, delete) on different - devices are allowed. A maximum of 100 devices can be specified per - invocation. For large scale operations, consider using the import - feature using blob - storage(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities). - - :param devices: - :type devices: list[~service.models.ExportImportDevice] - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: BulkRegistryOperationResult or ClientRawResponse if raw=true - :rtype: ~service.models.BulkRegistryOperationResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.bulk_create_or_update_devices.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(devices, '[ExportImportDevice]') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200, 400]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('BulkRegistryOperationResult', response) - if response.status_code == 400: - deserialized = self._deserialize('BulkRegistryOperationResult', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - bulk_create_or_update_devices.metadata = {'url': '/devices'} - - def query_iot_hub( - self, query_specification, custom_headers=None, raw=False, **operation_config): - """Query an IoT hub to retrieve information regarding device twins using a - SQL-like language. - - :param query_specification: - :type query_specification: ~service.models.QuerySpecification - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: QueryResult or ClientRawResponse if raw=true - :rtype: ~service.models.QueryResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.query_iot_hub.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(query_specification, 'QuerySpecification') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - # @digimaun - Changed from 'QueryResult' to [object] - if response.status_code == 200: - deserialized = self._deserialize('[object]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - # @digimaun - Added Custom - continuation = response.headers.get('x-ms-continuation') - return deserialized, continuation - - query_iot_hub.metadata = {'url': '/devices/query'} - - def get_device( - self, id, custom_headers=None, raw=False, **operation_config): - """Retrieve a device from the identity registry of an IoT hub. - - :param id: Device ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Device or ClientRawResponse if raw=true - :rtype: ~service.models.Device or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_device.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - # @digimaun - Change deserialize type to object from Device - deserialized = self._deserialize('object', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_device.metadata = {'url': '/devices/{id}'} - - def create_or_update_device( - self, id, device, if_match=None, custom_headers=None, raw=False, **operation_config): - """Create or update the identity of a device in the identity registry of - an IoT hub. - - Create or update the identity of a device in the identity registry of - an IoT hub. An ETag must not be specified for the create operation. An - ETag must be specified for the update operation. Note that generationId - and deviceId cannot be updated by the user. - - :param id: Device ID. - :type id: str - :param device: - :type device: ~service.models.Device - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Device or ClientRawResponse if raw=true - :rtype: ~service.models.Device or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.create_or_update_device.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(device, 'Device') - - # Construct and send request - request = self._client.put(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Device', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_or_update_device.metadata = {'url': '/devices/{id}'} - - def delete_device( - self, id, if_match=None, custom_headers=None, raw=False, **operation_config): - """Delete the identity of a device from the identity registry of an IoT - hub. - - Delete the identity of a device from the identity registry of an IoT - hub. This request requires the If-Match header. The client may specify - the ETag for the device identity on the request in order to compare to - the ETag maintained by the service for the purpose of optimistic - concurrency. The delete operation is performed only if the ETag sent by - the client matches the value maintained by the server, indicating that - the device identity has not been modified since it was retrieved by the - client. To force an unconditional delete, set If-Match to the wildcard - character (*). - - :param id: Device ID. - :type id: str - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.delete_device.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [204]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - delete_device.metadata = {'url': '/devices/{id}'} - - def apply_configuration_on_device( - self, id, content, custom_headers=None, raw=False, **operation_config): - """Applies the provided configuration content to the specified device. - - Applies the provided configuration content to the specified device. - Configuration content must have modules content. - - :param id: Device ID. - :type id: str - :param content: - :type content: ~service.models.ConfigurationContent - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.apply_configuration_on_device.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(content, 'ConfigurationContent') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200, 204]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('object', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - apply_configuration_on_device.metadata = {'url': '/devices/{id}/applyConfigurationContent'} - - def create_job( - self, job_properties, custom_headers=None, raw=False, **operation_config): - """Create a new job on an IoT hub. - - Create a new job on an IoT hub. See - https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities - for more information. - - :param job_properties: - :type job_properties: ~service.models.JobProperties - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: JobProperties or ClientRawResponse if raw=true - :rtype: ~service.models.JobProperties or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.create_job.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(job_properties, 'JobProperties') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('JobProperties', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_job.metadata = {'url': '/jobs/create'} - - def get_jobs( - self, custom_headers=None, raw=False, **operation_config): - """Gets the status of all jobs in an iot hub. - - Gets the status of all jobs in an iot hub. See - https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities - for more information. - - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~service.models.JobProperties] or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_jobs.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[JobProperties]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_jobs.metadata = {'url': '/jobs'} - - def get_job( - self, id, custom_headers=None, raw=False, **operation_config): - """Gets the status of job in an iot hub. - - Gets the status of job in an iot hub. See - https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities - for more information. - - :param id: Job ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: JobProperties or ClientRawResponse if raw=true - :rtype: ~service.models.JobProperties or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_job.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('JobProperties', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_job.metadata = {'url': '/jobs/{id}'} - - def cancel_job( - self, id, custom_headers=None, raw=False, **operation_config): - """Cancels job in an IoT hub. - - Cancels job in an IoT hub. See - https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities - for more information. - - :param id: Job ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.cancel_job.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200, 204]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('object', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - cancel_job.metadata = {'url': '/jobs/{id}'} - - def purge_command_queue( - self, id, custom_headers=None, raw=False, **operation_config): - """Delete all the pending commands for this device from the IoT hub. - - :param id: Device ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: PurgeMessageQueueResult or ClientRawResponse if raw=true - :rtype: ~service.models.PurgeMessageQueueResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.purge_command_queue.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('PurgeMessageQueueResult', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - purge_command_queue.metadata = {'url': '/devices/{id}/commands'} - - def get_twin( - self, id, custom_headers=None, raw=False, **operation_config): - """Get a device twin. - - Get a device twin. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins - for more information. - - :param id: Device ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Twin or ClientRawResponse if raw=true - :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_twin.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - # @digimaun - change device param from 'Twin' to {object} - if response.status_code == 200: - deserialized = self._deserialize('{object}', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_twin.metadata = {'url': '/twins/{id}'} - - def replace_twin( - self, id, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): - """Replaces tags and desired properties of a device twin. - - Replaces a device twin. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins - for more information. - - :param id: Device ID. - :type id: str - :param device_twin_info: Device twin info - :type device_twin_info: ~service.models.Twin - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Twin or ClientRawResponse if raw=true - :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.replace_twin.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - # @digimaun - Change deserialize type to {object} from Twin - body_content = self._serialize.body(device_twin_info, '{object}') - - # Construct and send request - request = self._client.put(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Twin', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - replace_twin.metadata = {'url': '/twins/{id}'} - - def update_twin( - self, id, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): - """Updates tags and desired properties of a device twin. - - Updates a device twin. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins - for more information. - - :param id: Device ID. - :type id: str - :param device_twin_info: Device twin info - :type device_twin_info: ~service.models.Twin - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Twin or ClientRawResponse if raw=true - :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.update_twin.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(device_twin_info, 'Twin') - - # Construct and send request - request = self._client.patch(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Twin', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - update_twin.metadata = {'url': '/twins/{id}'} - - def get_module_twin( - self, id, mid, custom_headers=None, raw=False, **operation_config): - """Gets a module twin. - - Gets a module twin. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins - for more information. - - :param id: Device ID. - :type id: str - :param mid: Module ID. - :type mid: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Twin or ClientRawResponse if raw=true - :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_module_twin.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - # @digimaun - Change deserialize type to {object} from Twin - deserialized = self._deserialize('{object}', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_module_twin.metadata = {'url': '/twins/{id}/modules/{mid}'} - - def replace_module_twin( - self, id, mid, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): - """Replaces tags and desired properties of a module twin. - - Replaces a module twin. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins - for more information. - - :param id: Device ID. - :type id: str - :param mid: Module ID. - :type mid: str - :param device_twin_info: Device twin info - :type device_twin_info: ~service.models.Twin - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Twin or ClientRawResponse if raw=true - :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.replace_module_twin.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - # @digimaun - Change deserialize type to {object} from Twin - body_content = self._serialize.body(device_twin_info, '{object}') - - # Construct and send request - request = self._client.put(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Twin', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - replace_module_twin.metadata = {'url': '/twins/{id}/modules/{mid}'} - - def update_module_twin( - self, id, mid, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): - """Updates tags and desired properties of a module twin. - - Updates a module twin. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins - for more information. - - :param id: Device ID. - :type id: str - :param mid: Module ID. - :type mid: str - :param device_twin_info: Device twin information - :type device_twin_info: ~service.models.Twin - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Twin or ClientRawResponse if raw=true - :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.update_module_twin.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(device_twin_info, 'Twin') - - # Construct and send request - request = self._client.patch(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Twin', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - update_module_twin.metadata = {'url': '/twins/{id}/modules/{mid}'} - - def send_device_command( - self, custom_headers=None, raw=False, **operation_config): - """ - - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.send_device_command.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('object', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - send_device_command.metadata = {'url': '/messages/deviceBound'} - - def get_job1( - self, id, custom_headers=None, raw=False, **operation_config): - """Retrieve details of an existing job from an IoT hub. - - :param id: Job ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: JobResponse or ClientRawResponse if raw=true - :rtype: ~service.models.JobResponse or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_job1.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - # @digimaun - altered from "JobResponse" to "{object}" - deserialized = self._deserialize('{object}', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_job1.metadata = {'url': '/jobs/v2/{id}'} - - def create_job1( - self, id, job_request, custom_headers=None, raw=False, **operation_config): - """Create a new job on an IoT hub. - - :param id: Job ID. - :type id: str - :param job_request: - :type job_request: ~service.models.JobRequest - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: JobResponse or ClientRawResponse if raw=true - :rtype: ~service.models.JobResponse or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.create_job1.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(job_request, 'JobRequest') - - # Construct and send request - request = self._client.put(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - # @digimaun - changed from "JobResponse" to {object} - if response.status_code == 200: - deserialized = self._deserialize('{object}', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_job1.metadata = {'url': '/jobs/v2/{id}'} - - def cancel_job1( - self, id, custom_headers=None, raw=False, **operation_config): - """Cancel an existing job on an IoT hub. - - :param id: Job ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: JobResponse or ClientRawResponse if raw=true - :rtype: ~service.models.JobResponse or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.cancel_job1.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - # @digimaun - changed from "JobResponse" to {object} - if response.status_code == 200: - deserialized = self._deserialize('{object}', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - cancel_job1.metadata = {'url': '/jobs/v2/{id}/cancel'} - - def query_jobs( - self, job_type=None, job_status=None, custom_headers=None, raw=False, **operation_config): - """Query an IoT hub to retrieve information regarding jobs using the IoT - Hub query language. - - :param job_type: Job Type. - :type job_type: str - :param job_status: Job Status. - :type job_status: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: QueryResult or ClientRawResponse if raw=true - :rtype: ~service.models.QueryResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.query_jobs.metadata['url'] - - # Construct parameters - query_parameters = {} - if job_type is not None: - query_parameters['jobType'] = self._serialize.query("job_type", job_type, 'str') - if job_status is not None: - query_parameters['jobStatus'] = self._serialize.query("job_status", job_status, 'str') - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - # @digimaun - changed from 'QueryResult' to [object] - if response.status_code == 200: - deserialized = self._deserialize('[object]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - # @digimaun - Added Custom - continuation = response.headers.get('x-ms-continuation') - return deserialized, continuation - query_jobs.metadata = {'url': '/jobs/v2/query'} - - def get_modules_on_device( - self, id, custom_headers=None, raw=False, **operation_config): - """Retrieve all the module identities on the device. - - :param id: Device ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~service.models.Module] or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_modules_on_device.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[Module]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_modules_on_device.metadata = {'url': '/devices/{id}/modules'} - - def get_module( - self, id, mid, custom_headers=None, raw=False, **operation_config): - """Retrieve the specified module identity on the device. - - :param id: Device ID. - :type id: str - :param mid: Module ID. - :type mid: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Module or ClientRawResponse if raw=true - :rtype: ~service.models.Module or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_module.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - # @digimaun - Change deserialize type to {object} from Module - deserialized = self._deserialize('{object}', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_module.metadata = {'url': '/devices/{id}/modules/{mid}'} - - def create_or_update_module( - self, id, mid, module, if_match=None, custom_headers=None, raw=False, **operation_config): - """Create or update the module identity for device in IoT hub. An ETag - must not be specified for the create operation. An ETag must be - specified for the update operation. Note that moduleId and generation - cannot be updated by the user. - - :param id: Device ID. - :type id: str - :param mid: Module ID. - :type mid: str - :param module: - :type module: ~service.models.Module - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Module or ClientRawResponse if raw=true - :rtype: ~service.models.Module or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.create_or_update_module.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(module, 'Module') - - # Construct and send request - request = self._client.put(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200, 201]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Module', response) - if response.status_code == 201: - deserialized = self._deserialize('Module', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_or_update_module.metadata = {'url': '/devices/{id}/modules/{mid}'} - - def delete_module( - self, id, mid, if_match=None, custom_headers=None, raw=False, **operation_config): - """Delete the module identity for device of an IoT hub. This request - requires the If-Match header. The client may specify the ETag for the - device identity on the request in order to compare to the ETag - maintained by the service for the purpose of optimistic concurrency. - The delete operation is performed only if the ETag sent by the client - matches the value maintained by the server, indicating that the device - identity has not been modified since it was retrieved by the client. To - force an unconditional delete, set If-Match to the wildcard character - (*). - - :param id: Device ID. - :type id: str - :param mid: Module ID. - :type mid: str - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.delete_module.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [204]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - delete_module.metadata = {'url': '/devices/{id}/modules/{mid}'} - - def invoke_device_method( - self, deviceid, direct_method_request, custom_headers=None, raw=False, **operation_config): - """Invoke a direct method on a device. - - Invoke a direct method on a device. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-direct-methods - for more information. - - :param deviceid: - :type deviceid: str - :param direct_method_request: - :type direct_method_request: ~service.models.CloudToDeviceMethod - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: CloudToDeviceMethodResult or ClientRawResponse if raw=true - :rtype: ~service.models.CloudToDeviceMethodResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.invoke_device_method.metadata['url'] - # digimaun - change device param from {id} to {deviceId} - - path_format_arguments = { - 'deviceId': self._serialize.url("deviceId", deviceid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(direct_method_request, 'CloudToDeviceMethod') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('CloudToDeviceMethodResult', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - - # @digimaun - change device param from {id} to {deviceId} - invoke_device_method.metadata = {'url': '/twins/{deviceId}/methods'} - - # @digimaun - invoke_module_method - def invoke_device_method1( - self, device_id, module_id, direct_method_request, custom_headers=None, raw=False, **operation_config): - """Invoke a direct method on a module of a device. - - Invoke a direct method on a device. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-direct-methods - for more information. - - :param device_id: - :type device_id: str - :param module_id: - :type module_id: str - :param direct_method_request: - :type direct_method_request: ~service.models.CloudToDeviceMethod - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: CloudToDeviceMethodResult or ClientRawResponse if raw=true - :rtype: ~service.models.CloudToDeviceMethodResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.invoke_device_method1.metadata['url'] - path_format_arguments = { - 'deviceId': self._serialize.url("device_id", device_id, 'str'), - 'moduleId': self._serialize.url("module_id", module_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(direct_method_request, 'CloudToDeviceMethod') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('CloudToDeviceMethodResult', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - - # @digimaun - change device param from {id} to {deviceId} - invoke_device_method1.metadata = {'url': '/twins/{deviceId}/modules/{moduleId}/methods'} - - def get_interfaces( - self, digital_twin_id, custom_headers=None, raw=False, **operation_config): - """Gets the list of interfaces. - - :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is - DeviceId[~ModuleId]. ModuleId is optional. - :type digital_twin_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: DigitalTwinInterfaces or ClientRawResponse if raw=true - :rtype: ~service.models.DigitalTwinInterfaces or - ~msrest.pipeline.ClientRawResponse - :raises: class:`CloudError` - """ - # Construct URL - url = self.get_interfaces.metadata['url'] - path_format_arguments = { - 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released - query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - header_dict = {} - - # @anusapan - deserialize as {object} from DigitalTwinInterfaces - if response.status_code == 200: - deserialized = self._deserialize('{object}', response) - header_dict = { - 'ETag': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - get_interfaces.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces'} - - def update_interfaces( - self, digital_twin_id, if_match=None, interfaces=None, custom_headers=None, raw=False, **operation_config): - """Updates desired properties of multiple interfaces. - Example URI: "digitalTwins/{digitalTwinId}/interfaces". - - :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is - DeviceId[~ModuleId]. ModuleId is optional. - :type digital_twin_id: str - :param if_match: - :type if_match: str - :param interfaces: Interface(s) data to patch in the digital twin. - :type interfaces: dict[str, - ~service.models.DigitalTwinInterfacesPatchInterfacesValue] - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: DigitalTwinInterfaces or ClientRawResponse if raw=true - :rtype: ~service.models.DigitalTwinInterfaces or - ~msrest.pipeline.ClientRawResponse - :raises: class:`CloudError` - """ - interfaces_patch_info = models.DigitalTwinInterfacesPatch(interfaces=interfaces) - - # Construct URL - url = self.update_interfaces.metadata['url'] - path_format_arguments = { - 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released - query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - - # Construct body - body_content = self._serialize.body(interfaces_patch_info, 'DigitalTwinInterfacesPatch') - - # Construct and send request - request = self._client.patch(url, query_parameters) - response = self._client.send(request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - - deserialized = None - header_dict = {} - - # @anusapan - deserialize as {object} from DigitalTwinInterfaces - if response.status_code == 200: - deserialized = self._deserialize('{object}', response) - header_dict = { - 'ETag': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - update_interfaces.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces'} - - def get_interface( - self, digital_twin_id, interface_name, custom_headers=None, raw=False, **operation_config): - """Gets the interface of given interfaceId. - Example URI: "digitalTwins/{digitalTwinId}/interfaces/{interfaceName}". - - :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is - DeviceId[~ModuleId]. ModuleId is optional. - :type digital_twin_id: str - :param interface_name: The interface name. - :type interface_name: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: DigitalTwinInterfaces or ClientRawResponse if raw=true - :rtype: ~service.models.DigitalTwinInterfaces or - ~msrest.pipeline.ClientRawResponse - :raises: class:`CloudError` - """ - # Construct URL - url = self.get_interface.metadata['url'] - path_format_arguments = { - 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str'), - 'interfaceName': self._serialize.url("interface_name", interface_name, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released - query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') - - # Construct headers - header_parameters = {} - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - header_dict = {} - - # @anusapan - deserialize as {object} from DigitalTwinInterfaces - if response.status_code == 200: - deserialized = self._deserialize('{object}', response) - header_dict = { - 'ETag': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - get_interface.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces/{interfaceName}'} - - def get_digital_twin_model( - self, model_id, expand=None, custom_headers=None, raw=False, **operation_config): - """Returns a DigitalTwin model definition for the given id. - If "expand" is present in the query parameters and id is for a device - capability model then it returns - the capability metamodel with expanded interface definitions. - - :param model_id: Model id Ex: - urn:contoso:TemperatureSensor:1 - :type model_id: str - :param expand: Indicates whether to expand the device capability - model's interface definitions inline or not. - This query parameter ONLY applies to Capability model. - :type expand: bool - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: class:`CloudError` - """ - # Construct URL - url = self.get_digital_twin_model.metadata['url'] - path_format_arguments = { - 'modelId': self._serialize.url("model_id", model_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - if expand is not None: - query_parameters['expand'] = self._serialize.query("expand", expand, 'bool') - # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released - query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') - - # Construct headers - header_parameters = {} - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('object', response) - header_dict = { - 'ETag': 'str', - 'x-ms-model-id': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - get_digital_twin_model.metadata = {'url': '/digitalTwins/models/{modelId}'} - - def invoke_interface_command( - self, digital_twin_id, interface_name, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config): - """Invoke a digital twin interface command. - - Invoke a digital twin interface command. - - :param digital_twin_id: - :type digital_twin_id: str - :param interface_name: - :type interface_name: str - :param command_name: - :type command_name: str - :param payload: - :type payload: object - :param connect_timeout_in_seconds: Connect timeout in seconds. - :type connect_timeout_in_seconds: int - :param response_timeout_in_seconds: Response timeout in seconds. - :type response_timeout_in_seconds: int - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: class:`CloudError` - """ - # Construct URL - url = self.invoke_interface_command.metadata['url'] - path_format_arguments = { - 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str'), - 'interfaceName': self._serialize.url("interface_name", interface_name, 'str'), - 'commandName': self._serialize.url("command_name", command_name, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released - query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str') - if connect_timeout_in_seconds is not None: - query_parameters['connectTimeoutInSeconds'] = self._serialize.query("connect_timeout_in_seconds", connect_timeout_in_seconds, 'int') - if response_timeout_in_seconds is not None: - query_parameters['responseTimeoutInSeconds'] = self._serialize.query("response_timeout_in_seconds", response_timeout_in_seconds, 'int') - - # Construct headers - header_parameters = {} - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(payload, 'object') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send(request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('object', response) - header_dict = { - 'x-ms-command-statuscode': 'int', - 'x-ms-request-id': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - invoke_interface_command.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces/{interfaceName}/commands/{commandName}'} diff --git a/azext_iot/sdk/service/models/__init__.py b/azext_iot/sdk/service/models/__init__.py deleted file mode 100644 index aa0335a88..000000000 --- a/azext_iot/sdk/service/models/__init__.py +++ /dev/null @@ -1,92 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .configuration_metrics import ConfigurationMetrics -from .configuration_content import ConfigurationContent -from .configuration import Configuration -from .configuration_queries_test_input import ConfigurationQueriesTestInput -from .configuration_queries_test_response import ConfigurationQueriesTestResponse -from .registry_statistics import RegistryStatistics -from .service_statistics import ServiceStatistics -from .symmetric_key import SymmetricKey -from .x509_thumbprint import X509Thumbprint -from .authentication_mechanism import AuthenticationMechanism -from .device_capabilities import DeviceCapabilities -from .device import Device -from .property_container import PropertyContainer -from .export_import_device import ExportImportDevice -from .device_registry_operation_error import DeviceRegistryOperationError -from .device_registry_operation_warning import DeviceRegistryOperationWarning -from .bulk_registry_operation_result import BulkRegistryOperationResult -from .query_specification import QuerySpecification -from .query_result import QueryResult -from .job_properties import JobProperties -from .purge_message_queue_result import PurgeMessageQueueResult -from .twin_properties import TwinProperties -from .twin import Twin -from .cloud_to_device_method import CloudToDeviceMethod -from .job_request import JobRequest -from .device_job_statistics import DeviceJobStatistics -from .job_response import JobResponse -from .module import Module -from .cloud_to_device_method_result import CloudToDeviceMethodResult -from .desired import Desired -from .desired_state import DesiredState -from .digital_twin_interfaces import DigitalTwinInterfaces -from .digital_twin_interfaces_patch import DigitalTwinInterfacesPatch -from .digital_twin_interfaces_patch_interfaces_value import DigitalTwinInterfacesPatchInterfacesValue -from .digital_twin_interfaces_patch_interfaces_value_properties_value import DigitalTwinInterfacesPatchInterfacesValuePropertiesValue -from .digital_twin_interfaces_patch_interfaces_value_properties_value_desired import DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired -from .interface import Interface -from .property import Property -from .reported import Reported - -__all__ = [ - 'ConfigurationMetrics', - 'ConfigurationContent', - 'Configuration', - 'ConfigurationQueriesTestInput', - 'ConfigurationQueriesTestResponse', - 'RegistryStatistics', - 'ServiceStatistics', - 'SymmetricKey', - 'X509Thumbprint', - 'AuthenticationMechanism', - 'DeviceCapabilities', - 'Device', - 'PropertyContainer', - 'ExportImportDevice', - 'DeviceRegistryOperationError', - 'DeviceRegistryOperationWarning', - 'BulkRegistryOperationResult', - 'QuerySpecification', - 'QueryResult', - 'JobProperties', - 'PurgeMessageQueueResult', - 'TwinProperties', - 'Twin', - 'CloudToDeviceMethod', - 'JobRequest', - 'DeviceJobStatistics', - 'JobResponse', - 'Module', - 'CloudToDeviceMethodResult', - 'Desired', - 'DigitalTwinInterfaces', - 'DigitalTwinInterfacesPatch', - 'DigitalTwinInterfacesPatchInterfacesValue', - 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValue', - 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired', - 'DesiredState', - 'Interface', - 'Property', - 'Reported', -] diff --git a/azext_iot/sdk/service/models/authentication_mechanism.py b/azext_iot/sdk/service/models/authentication_mechanism.py deleted file mode 100644 index 5344b5f17..000000000 --- a/azext_iot/sdk/service/models/authentication_mechanism.py +++ /dev/null @@ -1,37 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class AuthenticationMechanism(Model): - """AuthenticationMechanism. - - :param symmetric_key: - :type symmetric_key: ~service.models.SymmetricKey - :param x509_thumbprint: - :type x509_thumbprint: ~service.models.X509Thumbprint - :param type: Possible values include: 'sas', 'selfSigned', - 'certificateAuthority', 'none' - :type type: str or ~service.models.enum - """ - - _attribute_map = { - 'symmetric_key': {'key': 'symmetricKey', 'type': 'SymmetricKey'}, - 'x509_thumbprint': {'key': 'x509Thumbprint', 'type': 'X509Thumbprint'}, - 'type': {'key': 'type', 'type': 'str'}, - } - - def __init__(self, symmetric_key=None, x509_thumbprint=None, type=None): - super(AuthenticationMechanism, self).__init__() - self.symmetric_key = symmetric_key - self.x509_thumbprint = x509_thumbprint - self.type = type diff --git a/azext_iot/sdk/service/models/bulk_registry_operation_result.py b/azext_iot/sdk/service/models/bulk_registry_operation_result.py deleted file mode 100644 index 1651a0ce7..000000000 --- a/azext_iot/sdk/service/models/bulk_registry_operation_result.py +++ /dev/null @@ -1,38 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class BulkRegistryOperationResult(Model): - """Encapsulates the result of a bulk registry operation. - - :param is_successful: Whether or not the operation was successful. - :type is_successful: bool - :param errors: If the operation was not successful, this contains an array - of DeviceRegistryOperationError objects. - :type errors: list[~service.models.DeviceRegistryOperationError] - :param warnings: If the operation was partially successful, this contains - an array of DeviceRegistryOperationWarning objects. - :type warnings: list[~service.models.DeviceRegistryOperationWarning] - """ - - _attribute_map = { - 'is_successful': {'key': 'isSuccessful', 'type': 'bool'}, - 'errors': {'key': 'errors', 'type': '[DeviceRegistryOperationError]'}, - 'warnings': {'key': 'warnings', 'type': '[DeviceRegistryOperationWarning]'}, - } - - def __init__(self, is_successful=None, errors=None, warnings=None): - super(BulkRegistryOperationResult, self).__init__() - self.is_successful = is_successful - self.errors = errors - self.warnings = warnings diff --git a/azext_iot/sdk/service/models/cloud_to_device_method.py b/azext_iot/sdk/service/models/cloud_to_device_method.py deleted file mode 100644 index 8b74b4b7e..000000000 --- a/azext_iot/sdk/service/models/cloud_to_device_method.py +++ /dev/null @@ -1,50 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class CloudToDeviceMethod(Model): - """Parameters to execute a direct method on the device. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :param method_name: Method to run - :type method_name: str - :ivar payload: Payload - :vartype payload: object - :param response_timeout_in_seconds: - :type response_timeout_in_seconds: int - :param connect_timeout_in_seconds: - :type connect_timeout_in_seconds: int - """ - - # @digimaun - does not serialize payload if payload.readonly = True - # _validation = { - # 'payload': {'readonly': True}, - # } - - # @digimaun - payload altered from '{object}' to 'object' since primitives can be passed in. - _attribute_map = { - 'method_name': {'key': 'methodName', 'type': 'str'}, - 'payload': {'key': 'payload', 'type': 'object'}, - 'response_timeout_in_seconds': {'key': 'responseTimeoutInSeconds', 'type': 'int'}, - 'connect_timeout_in_seconds': {'key': 'connectTimeoutInSeconds', 'type': 'int'}, - } - - # @digimaun - added payload - def __init__(self, method_name=None, response_timeout_in_seconds=None, connect_timeout_in_seconds=None, payload=None): - super(CloudToDeviceMethod, self).__init__() - self.method_name = method_name - self.payload = payload - self.response_timeout_in_seconds = response_timeout_in_seconds - self.connect_timeout_in_seconds = connect_timeout_in_seconds diff --git a/azext_iot/sdk/service/models/cloud_to_device_method_result.py b/azext_iot/sdk/service/models/cloud_to_device_method_result.py deleted file mode 100644 index d6a3969c7..000000000 --- a/azext_iot/sdk/service/models/cloud_to_device_method_result.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class CloudToDeviceMethodResult(Model): - """Represents the Device Method Invocation Results. - - :param status: Method invocation result status. - :type status: int - :param payload: Method invocation result payload. - :type payload: object - """ - - _attribute_map = { - 'status': {'key': 'status', 'type': 'int'}, - 'payload': {'key': 'payload', 'type': 'object'}, - } - - def __init__(self, status=None, payload=None): - super(CloudToDeviceMethodResult, self).__init__() - self.status = status - self.payload = payload diff --git a/azext_iot/sdk/service/models/configuration.py b/azext_iot/sdk/service/models/configuration.py deleted file mode 100644 index 3da1e54c8..000000000 --- a/azext_iot/sdk/service/models/configuration.py +++ /dev/null @@ -1,72 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Configuration(Model): - """Configuration for IotHub devices and modules. - - :param id: Gets Identifier for the configuration - :type id: str - :param schema_version: Gets Schema version for the configuration - :type schema_version: str - :param labels: Gets or sets labels for the configuration - :type labels: dict[str, str] - :param content: Gets or sets Content for the configuration - :type content: ~service.models.ConfigurationContent - :param target_condition: Gets or sets Target Condition for the - configuration - :type target_condition: str - :param created_time_utc: Gets creation time for the configuration - :type created_time_utc: datetime - :param last_updated_time_utc: Gets last update time for the configuration - :type last_updated_time_utc: datetime - :param priority: Gets or sets Priority for the configuration - :type priority: int - :param system_metrics: System Configuration Metrics - :type system_metrics: ~service.models.ConfigurationMetrics - :param metrics: Custom Configuration Metrics - :type metrics: ~service.models.ConfigurationMetrics - :param etag: Gets or sets configuration's ETag - :type etag: str - """ - - # @digimaun - added missing contentType property - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'schema_version': {'key': 'schemaVersion', 'type': 'str'}, - 'labels': {'key': 'labels', 'type': '{str}'}, - 'content': {'key': 'content', 'type': 'ConfigurationContent'}, - 'target_condition': {'key': 'targetCondition', 'type': 'str'}, - 'created_time_utc': {'key': 'createdTimeUtc', 'type': 'iso-8601'}, - 'last_updated_time_utc': {'key': 'lastUpdatedTimeUtc', 'type': 'iso-8601'}, - 'priority': {'key': 'priority', 'type': 'int'}, - 'system_metrics': {'key': 'systemMetrics', 'type': 'ConfigurationMetrics'}, - 'metrics': {'key': 'metrics', 'type': 'ConfigurationMetrics'}, - 'etag': {'key': 'etag', 'type': 'str'}, - 'content_type': {'key': 'contentType', 'type': 'str'}, - } - - def __init__(self, id=None, schema_version=None, labels=None, content=None, target_condition=None, created_time_utc=None, last_updated_time_utc=None, priority=None, system_metrics=None, metrics=None, etag=None, content_type=None): - super(Configuration, self).__init__() - self.id = id - self.schema_version = schema_version - self.labels = labels - self.content = content - self.target_condition = target_condition - self.created_time_utc = created_time_utc - self.last_updated_time_utc = last_updated_time_utc - self.priority = priority - self.system_metrics = system_metrics - self.metrics = metrics - self.etag = etag - self.content_type = content_type diff --git a/azext_iot/sdk/service/models/configuration_content.py b/azext_iot/sdk/service/models/configuration_content.py deleted file mode 100644 index 6445ef122..000000000 --- a/azext_iot/sdk/service/models/configuration_content.py +++ /dev/null @@ -1,36 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ConfigurationContent(Model): - """Configuration Content for Devices or Modules on Edge Devices. - - :param device_content: Gets or sets device Configurations - :type device_content: dict[str, object] - :param modules_content: Gets or sets Module Configurations - :type modules_content: dict[str, dict[str, object]] - """ - # @digimaun altered modulesContent type from {{object}} to {object} - # @digimaun added moduleContent - - _attribute_map = { - 'device_content': {'key': 'deviceContent', 'type': '{object}'}, - 'modules_content': {'key': 'modulesContent', 'type': '{object}'}, - 'module_content': {'key': 'moduleContent', 'type': '{object}'} - } - - def __init__(self, device_content=None, modules_content=None, module_content=None): - super(ConfigurationContent, self).__init__() - self.device_content = device_content - self.modules_content = modules_content - self.module_content = module_content diff --git a/azext_iot/sdk/service/models/configuration_metrics.py b/azext_iot/sdk/service/models/configuration_metrics.py deleted file mode 100644 index 332bb17d7..000000000 --- a/azext_iot/sdk/service/models/configuration_metrics.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ConfigurationMetrics(Model): - """Configuration Metrics. - - :param results: - :type results: dict[str, long] - :param queries: - :type queries: dict[str, str] - """ - - _attribute_map = { - 'results': {'key': 'results', 'type': '{long}'}, - 'queries': {'key': 'queries', 'type': '{str}'}, - } - - def __init__(self, results=None, queries=None): - super(ConfigurationMetrics, self).__init__() - self.results = results - self.queries = queries diff --git a/azext_iot/sdk/service/models/configuration_queries_test_input.py b/azext_iot/sdk/service/models/configuration_queries_test_input.py deleted file mode 100644 index 978df2228..000000000 --- a/azext_iot/sdk/service/models/configuration_queries_test_input.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ConfigurationQueriesTestInput(Model): - """ConfigurationQueriesTestInput. - - :param target_condition: - :type target_condition: str - :param custom_metric_queries: - :type custom_metric_queries: dict[str, str] - """ - - _attribute_map = { - 'target_condition': {'key': 'targetCondition', 'type': 'str'}, - 'custom_metric_queries': {'key': 'customMetricQueries', 'type': '{str}'}, - } - - def __init__(self, target_condition=None, custom_metric_queries=None): - super(ConfigurationQueriesTestInput, self).__init__() - self.target_condition = target_condition - self.custom_metric_queries = custom_metric_queries diff --git a/azext_iot/sdk/service/models/configuration_queries_test_response.py b/azext_iot/sdk/service/models/configuration_queries_test_response.py deleted file mode 100644 index 43f1d5ae5..000000000 --- a/azext_iot/sdk/service/models/configuration_queries_test_response.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ConfigurationQueriesTestResponse(Model): - """ConfigurationQueriesTestResponse. - - :param target_condition_error: - :type target_condition_error: str - :param custom_metric_query_errors: - :type custom_metric_query_errors: dict[str, str] - """ - - _attribute_map = { - 'target_condition_error': {'key': 'targetConditionError', 'type': 'str'}, - 'custom_metric_query_errors': {'key': 'customMetricQueryErrors', 'type': '{str}'}, - } - - def __init__(self, target_condition_error=None, custom_metric_query_errors=None): - super(ConfigurationQueriesTestResponse, self).__init__() - self.target_condition_error = target_condition_error - self.custom_metric_query_errors = custom_metric_query_errors diff --git a/azext_iot/sdk/service/models/desired.py b/azext_iot/sdk/service/models/desired.py deleted file mode 100644 index 8ac77f637..000000000 --- a/azext_iot/sdk/service/models/desired.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Desired(Model): - """Desired. - - :param value: The desired value of the interface property to set in a - digitalTwin. - :type value: object - """ - - _attribute_map = { - 'value': {'key': 'value', 'type': 'object'}, - } - - def __init__(self, value=None): - super(Desired, self).__init__() - self.value = value diff --git a/azext_iot/sdk/service/models/desired_state.py b/azext_iot/sdk/service/models/desired_state.py deleted file mode 100644 index 5c29a0445..000000000 --- a/azext_iot/sdk/service/models/desired_state.py +++ /dev/null @@ -1,40 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DesiredState(Model): - """DesiredState. - - :param code: Status code for the operation. - :type code: int - :param sub_code: Sub status code for the status. - :type sub_code: int - :param version: Version of the desired value received. - :type version: long - :param description: Description of the status. - :type description: str - """ - - _attribute_map = { - 'code': {'key': 'code', 'type': 'int'}, - 'sub_code': {'key': 'subCode', 'type': 'int'}, - 'version': {'key': 'version', 'type': 'long'}, - 'description': {'key': 'description', 'type': 'str'}, - } - - def __init__(self, code=None, sub_code=None, version=None, description=None): - super(DesiredState, self).__init__() - self.code = code - self.sub_code = sub_code - self.version = version - self.description = description diff --git a/azext_iot/sdk/service/models/device.py b/azext_iot/sdk/service/models/device.py deleted file mode 100644 index d6a74d016..000000000 --- a/azext_iot/sdk/service/models/device.py +++ /dev/null @@ -1,77 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Device(Model): - """Device. - - :param device_id: - :type device_id: str - :param generation_id: - :type generation_id: str - :param etag: - :type etag: str - :param connection_state: Possible values include: 'Disconnected', - 'Connected' - :type connection_state: str or ~service.models.enum - :param status: Possible values include: 'enabled', 'disabled' - :type status: str or ~service.models.enum - :param status_reason: - :type status_reason: str - :param connection_state_updated_time: - :type connection_state_updated_time: datetime - :param status_updated_time: - :type status_updated_time: datetime - :param last_activity_time: - :type last_activity_time: datetime - :param cloud_to_device_message_count: - :type cloud_to_device_message_count: int - :param authentication: - :type authentication: ~service.models.AuthenticationMechanism - :param capabilities: - :type capabilities: ~service.models.DeviceCapabilities - :param device_scope: - :type device_scope: str - """ - - _attribute_map = { - 'device_id': {'key': 'deviceId', 'type': 'str'}, - 'generation_id': {'key': 'generationId', 'type': 'str'}, - 'etag': {'key': 'etag', 'type': 'str'}, - 'connection_state': {'key': 'connectionState', 'type': 'str'}, - 'status': {'key': 'status', 'type': 'str'}, - 'status_reason': {'key': 'statusReason', 'type': 'str'}, - 'connection_state_updated_time': {'key': 'connectionStateUpdatedTime', 'type': 'iso-8601'}, - 'status_updated_time': {'key': 'statusUpdatedTime', 'type': 'iso-8601'}, - 'last_activity_time': {'key': 'lastActivityTime', 'type': 'iso-8601'}, - 'cloud_to_device_message_count': {'key': 'cloudToDeviceMessageCount', 'type': 'int'}, - 'authentication': {'key': 'authentication', 'type': 'AuthenticationMechanism'}, - 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, - 'device_scope': {'key': 'deviceScope', 'type': 'str'}, - } - - def __init__(self, device_id=None, generation_id=None, etag=None, connection_state=None, status=None, status_reason=None, connection_state_updated_time=None, status_updated_time=None, last_activity_time=None, cloud_to_device_message_count=None, authentication=None, capabilities=None, device_scope=None): - super(Device, self).__init__() - self.device_id = device_id - self.generation_id = generation_id - self.etag = etag - self.connection_state = connection_state - self.status = status - self.status_reason = status_reason - self.connection_state_updated_time = connection_state_updated_time - self.status_updated_time = status_updated_time - self.last_activity_time = last_activity_time - self.cloud_to_device_message_count = cloud_to_device_message_count - self.authentication = authentication - self.capabilities = capabilities - self.device_scope = device_scope diff --git a/azext_iot/sdk/service/models/device_capabilities.py b/azext_iot/sdk/service/models/device_capabilities.py deleted file mode 100644 index 5c5e4207a..000000000 --- a/azext_iot/sdk/service/models/device_capabilities.py +++ /dev/null @@ -1,28 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DeviceCapabilities(Model): - """Status of Capabilities enabled on the device. - - :param iot_edge: - :type iot_edge: bool - """ - - _attribute_map = { - 'iot_edge': {'key': 'iotEdge', 'type': 'bool'}, - } - - def __init__(self, iot_edge=None): - super(DeviceCapabilities, self).__init__() - self.iot_edge = iot_edge diff --git a/azext_iot/sdk/service/models/device_job_statistics.py b/azext_iot/sdk/service/models/device_job_statistics.py deleted file mode 100644 index 5bb46680d..000000000 --- a/azext_iot/sdk/service/models/device_job_statistics.py +++ /dev/null @@ -1,44 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DeviceJobStatistics(Model): - """The job counts, e.g., number of failed/succeeded devices. - - :param device_count: Number of devices in the job - :type device_count: int - :param failed_count: The number of failed jobs - :type failed_count: int - :param succeeded_count: The number of Successed jobs - :type succeeded_count: int - :param running_count: The number of running jobs - :type running_count: int - :param pending_count: The number of pending (scheduled) jobs - :type pending_count: int - """ - - _attribute_map = { - 'device_count': {'key': 'deviceCount', 'type': 'int'}, - 'failed_count': {'key': 'failedCount', 'type': 'int'}, - 'succeeded_count': {'key': 'succeededCount', 'type': 'int'}, - 'running_count': {'key': 'runningCount', 'type': 'int'}, - 'pending_count': {'key': 'pendingCount', 'type': 'int'}, - } - - def __init__(self, device_count=None, failed_count=None, succeeded_count=None, running_count=None, pending_count=None): - super(DeviceJobStatistics, self).__init__() - self.device_count = device_count - self.failed_count = failed_count - self.succeeded_count = succeeded_count - self.running_count = running_count - self.pending_count = pending_count diff --git a/azext_iot/sdk/service/models/device_registry_operation_error.py b/azext_iot/sdk/service/models/device_registry_operation_error.py deleted file mode 100644 index af658192e..000000000 --- a/azext_iot/sdk/service/models/device_registry_operation_error.py +++ /dev/null @@ -1,119 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DeviceRegistryOperationError(Model): - """Encapsulates device registry operation error details. - - :param device_id: The ID of the device that indicated the error. - :type device_id: str - :param error_code: ErrorCode associated with the error. Possible values - include: 'InvalidErrorCode', 'GenericBadRequest', - 'InvalidProtocolVersion', 'DeviceInvalidResultCount', 'InvalidOperation', - 'ArgumentInvalid', 'ArgumentNull', 'IotHubFormatError', - 'DeviceStorageEntitySerializationError', 'BlobContainerValidationError', - 'ImportWarningExistsError', 'InvalidSchemaVersion', - 'DeviceDefinedMultipleTimes', 'DeserializationError', - 'BulkRegistryOperationFailure', 'DefaultStorageEndpointNotConfigured', - 'InvalidFileUploadCorrelationId', 'ExpiredFileUploadCorrelationId', - 'InvalidStorageEndpoint', 'InvalidMessagingEndpoint', - 'InvalidFileUploadCompletionStatus', 'InvalidStorageEndpointOrBlob', - 'RequestCanceled', 'InvalidStorageEndpointProperty', 'EtagDoesNotMatch', - 'RequestTimedOut', 'UnsupportedOperationOnReplica', 'NullMessage', - 'ConnectionForcefullyClosedOnNewConnection', 'InvalidDeviceScope', - 'InvalidRouteTestInput', 'InvalidSourceOnRoute', 'RoutingNotEnabled', - 'InvalidEndorsementKey', 'InvalidRegistrationId', 'InvalidStorageRootKey', - 'InvalidEnrollmentGroupId', 'TooManyEnrollments', - 'RegistrationIdDefinedMultipleTimes', 'CustomAllocationFailed', - 'CustomAllocationIotHubNotSpecified', - 'CustomAllocationUnauthorizedAccess', 'CannotRegisterModuleToModule', - 'TenantHubRoutingNotEnabled', 'InvalidConfigurationTargetCondition', - 'InvalidConfigurationContent', - 'CannotModifyImmutableConfigurationContent', - 'InvalidConfigurationCustomMetricsQuery', 'InvalidPnPInterfaceDefinition', - 'InvalidPnPDesiredProperties', 'InvalidPnPReportedProperties', - 'InvalidPnPWritableReportedProperties', 'GenericUnauthorized', - 'IotHubNotFound', 'IotHubUnauthorizedAccess', 'IotHubUnauthorized', - 'ElasticPoolNotFound', 'SystemModuleModifyUnauthorizedAccess', - 'GenericForbidden', 'IotHubSuspended', 'IotHubQuotaExceeded', - 'JobQuotaExceeded', 'DeviceMaximumQueueDepthExceeded', - 'IotHubMaxCbsTokenExceeded', 'DeviceMaximumActiveFileUploadLimitExceeded', - 'DeviceMaximumQueueSizeExceeded', 'RoutingEndpointResponseForbidden', - 'InvalidMessageExpiryTime', 'OperationNotAvailableInCurrentTier', - 'DeviceModelMaxPropertiesExceeded', - 'DeviceModelMaxIndexablePropertiesExceeded', 'IotDpsSuspended', - 'IotDpsSuspending', 'GenericNotFound', 'DeviceNotFound', 'JobNotFound', - 'QuotaMetricNotFound', 'SystemPropertyNotFound', 'AmqpAddressNotFound', - 'RoutingEndpointResponseNotFound', 'CertificateNotFound', - 'ElasticPoolTenantHubNotFound', 'ModuleNotFound', - 'AzureTableStoreNotFound', 'IotHubFailingOver', 'FeatureNotSupported', - 'QueryStoreClusterNotFound', 'DeviceNotOnline', - 'DeviceConnectionClosedRemotely', 'EnrollmentNotFound', - 'DeviceRegistrationNotFound', 'AsyncOperationNotFound', - 'EnrollmentGroupNotFound', 'ConfigurationNotFound', 'GroupNotFound', - 'GenericMethodNotAllowed', 'OperationNotAllowedInCurrentState', - 'ImportDevicesNotSupported', 'BulkAddDevicesNotSupported', - 'GenericConflict', 'DeviceAlreadyExists', 'LinkCreationConflict', - 'CallbackSubscriptionConflict', 'ModelAlreadyExists', 'DeviceLocked', - 'DeviceJobAlreadyExists', 'JobAlreadyExists', 'EnrollmentConflict', - 'EnrollmentGroupConflict', 'RegistrationStatusConflict', - 'ModuleAlreadyExistsOnDevice', 'ConfigurationAlreadyExists', - 'ApplyConfigurationAlreadyInProgressOnDevice', - 'GenericPreconditionFailed', 'PreconditionFailed', - 'DeviceMessageLockLost', 'JobRunPreconditionFailed', - 'InflightMessagesInLink', 'GenericRequestEntityTooLarge', - 'MessageTooLarge', 'TooManyDevices', 'TooManyModulesOnDevice', - 'ConfigurationCountLimitExceeded', 'GenericUnsupportedMediaType', - 'IncompatibleDataType', 'GenericTooManyRequests', 'ThrottlingException', - 'ThrottleBacklogLimitExceeded', 'ThrottlingBacklogTimeout', - 'ThrottlingMaxActiveJobCountExceeded', 'GenericServerError', - 'ServerError', 'JobCancelled', 'StatisticsRetrievalError', - 'ConnectionForcefullyClosed', 'InvalidBlobState', 'BackupTimedOut', - 'AzureStorageTimeout', 'GenericTimeout', 'InvalidThrottleParameter', - 'EventHubLinkAlreadyClosed', 'ReliableBlobStoreError', - 'RetryAttemptsExhausted', 'AzureTableStoreError', - 'CheckpointStoreNotFound', 'DocumentDbInvalidReturnValue', - 'ReliableDocDbStoreStoreError', 'ReliableBlobStoreTimeoutError', - 'ConfigReadFailed', 'InvalidContainerReceiveLink', - 'InvalidPartitionEpoch', 'RestoreTimedOut', 'StreamReservationFailure', - 'UnexpectedPropertyValue', 'OrchestrationOperationFailed', - 'GenericBadGateway', 'InvalidResponseWhileProxying', - 'GenericServiceUnavailable', 'ServiceUnavailable', 'PartitionNotFound', - 'IotHubActivationFailed', 'ServerBusy', 'IotHubRestoring', - 'ReceiveLinkOpensThrottled', 'ConnectionUnavailable', 'DeviceUnavailable', - 'ConfigurationNotAvailable', 'GroupNotAvailable', 'GenericGatewayTimeout', - 'GatewayTimeout' - :type error_code: str or ~service.models.enum - :param error_status: Additional details associated with the error. - :type error_status: str - :param module_id: - :type module_id: str - :param operation: - :type operation: str - """ - - _attribute_map = { - 'device_id': {'key': 'deviceId', 'type': 'str'}, - 'error_code': {'key': 'errorCode', 'type': 'str'}, - 'error_status': {'key': 'errorStatus', 'type': 'str'}, - 'module_id': {'key': 'moduleId', 'type': 'str'}, - 'operation': {'key': 'operation', 'type': 'str'}, - } - - def __init__(self, device_id=None, error_code=None, error_status=None, module_id=None, operation=None): - super(DeviceRegistryOperationError, self).__init__() - self.device_id = device_id - self.error_code = error_code - self.error_status = error_status - self.module_id = module_id - self.operation = operation diff --git a/azext_iot/sdk/service/models/device_registry_operation_warning.py b/azext_iot/sdk/service/models/device_registry_operation_warning.py deleted file mode 100644 index c3c0d47ee..000000000 --- a/azext_iot/sdk/service/models/device_registry_operation_warning.py +++ /dev/null @@ -1,37 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DeviceRegistryOperationWarning(Model): - """Encapsulates device registry operation error details. - - :param device_id: The ID of the device that indicated the warning. - :type device_id: str - :param warning_code: Possible values include: - 'DeviceRegisteredWithoutTwin' - :type warning_code: str or ~service.models.enum - :param warning_status: Additional details associated with the warning. - :type warning_status: str - """ - - _attribute_map = { - 'device_id': {'key': 'deviceId', 'type': 'str'}, - 'warning_code': {'key': 'warningCode', 'type': 'str'}, - 'warning_status': {'key': 'warningStatus', 'type': 'str'}, - } - - def __init__(self, device_id=None, warning_code=None, warning_status=None): - super(DeviceRegistryOperationWarning, self).__init__() - self.device_id = device_id - self.warning_code = warning_code - self.warning_status = warning_status diff --git a/azext_iot/sdk/service/models/digital_twin_interfaces.py b/azext_iot/sdk/service/models/digital_twin_interfaces.py deleted file mode 100644 index cac98248a..000000000 --- a/azext_iot/sdk/service/models/digital_twin_interfaces.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfaces(Model): - """DigitalTwinInterfaces. - - :param interfaces: Interface(s) data on the digital twin. - :type interfaces: dict[str, ~service.models.Interface] - :param version: Version of digital twin. - :type version: long - """ - - _attribute_map = { - 'interfaces': {'key': 'interfaces', 'type': '{Interface}'}, - 'version': {'key': 'version', 'type': 'long'}, - } - - def __init__(self, interfaces=None, version=None): - super(DigitalTwinInterfaces, self).__init__() - self.interfaces = interfaces - self.version = version diff --git a/azext_iot/sdk/service/models/digital_twin_interfaces_patch.py b/azext_iot/sdk/service/models/digital_twin_interfaces_patch.py deleted file mode 100644 index 512ff1729..000000000 --- a/azext_iot/sdk/service/models/digital_twin_interfaces_patch.py +++ /dev/null @@ -1,28 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatch(Model): - """DigitalTwinInterfacesPatch. - - :param interfaces: Interface(s) data to patch in the digital twin. - :type interfaces: dict[str, ~service.models.DigitalTwinInterfacesPatchInterfacesValue] - """ - - _attribute_map = { - 'interfaces': {'key': 'interfaces', 'type': '{DigitalTwinInterfacesPatchInterfacesValue}'}, - } - - def __init__(self, interfaces=None): - super(DigitalTwinInterfacesPatch, self).__init__() - self.interfaces = interfaces diff --git a/azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value.py b/azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value.py deleted file mode 100644 index 1ce39941b..000000000 --- a/azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value.py +++ /dev/null @@ -1,28 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatchInterfacesValue(Model): - """DigitalTwinInterfacesPatchInterfacesValue. - - :param properties: List of properties to update in an interface. - :type properties: dict[str, ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValue] - """ - - _attribute_map = { - 'properties': {'key': 'properties', 'type': '{DigitalTwinInterfacesPatchInterfacesValuePropertiesValue}'}, - } - - def __init__(self, properties=None): - super(DigitalTwinInterfacesPatchInterfacesValue, self).__init__() - self.properties = properties diff --git a/azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py b/azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py deleted file mode 100644 index c60a512bd..000000000 --- a/azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py +++ /dev/null @@ -1,28 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatchInterfacesValuePropertiesValue(Model): - """DigitalTwinInterfacesPatchInterfacesValuePropertiesValue. - - :param desired: - :type desired: ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired - """ - - _attribute_map = { - 'desired': {'key': 'desired', 'type': 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired'}, - } - - def __init__(self, desired=None): - super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValue, self).__init__() - self.desired = desired diff --git a/azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py b/azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py deleted file mode 100644 index 1e90b4098..000000000 --- a/azext_iot/sdk/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired(Model): - """DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired. - - :param value: The desired value of the interface property to set in a - digitalTwin. - :type value: object - """ - - _attribute_map = { - 'value': {'key': 'value', 'type': 'object'}, - } - - def __init__(self, value=None): - super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired, self).__init__() - self.value = value diff --git a/azext_iot/sdk/service/models/export_import_device.py b/azext_iot/sdk/service/models/export_import_device.py deleted file mode 100644 index afa4cb693..000000000 --- a/azext_iot/sdk/service/models/export_import_device.py +++ /dev/null @@ -1,70 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ExportImportDevice(Model): - """ExportImportDevice. - - :param id: - :type id: str - :param module_id: - :type module_id: str - :param e_tag: - :type e_tag: str - :param import_mode: Possible values include: 'createOrUpdate', 'create', - 'update', 'updateIfMatchETag', 'createOrUpdateIfMatchETag', 'delete', - 'deleteIfMatchETag', 'updateTwin', 'updateTwinIfMatchETag' - :type import_mode: str or ~service.models.enum - :param status: Possible values include: 'enabled', 'disabled' - :type status: str or ~service.models.enum - :param status_reason: - :type status_reason: str - :param authentication: - :type authentication: ~service.models.AuthenticationMechanism - :param twin_etag: - :type twin_etag: str - :param tags: - :type tags: dict[str, object] - :param properties: - :type properties: ~service.models.PropertyContainer - :param capabilities: - :type capabilities: ~service.models.DeviceCapabilities - """ - - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'module_id': {'key': 'moduleId', 'type': 'str'}, - 'e_tag': {'key': 'eTag', 'type': 'str'}, - 'import_mode': {'key': 'importMode', 'type': 'str'}, - 'status': {'key': 'status', 'type': 'str'}, - 'status_reason': {'key': 'statusReason', 'type': 'str'}, - 'authentication': {'key': 'authentication', 'type': 'AuthenticationMechanism'}, - 'twin_etag': {'key': 'twinETag', 'type': 'str'}, - 'tags': {'key': 'tags', 'type': '{object}'}, - 'properties': {'key': 'properties', 'type': 'PropertyContainer'}, - 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, - } - - def __init__(self, id=None, module_id=None, e_tag=None, import_mode=None, status=None, status_reason=None, authentication=None, twin_etag=None, tags=None, properties=None, capabilities=None): - super(ExportImportDevice, self).__init__() - self.id = id - self.module_id = module_id - self.e_tag = e_tag - self.import_mode = import_mode - self.status = status - self.status_reason = status_reason - self.authentication = authentication - self.twin_etag = twin_etag - self.tags = tags - self.properties = properties - self.capabilities = capabilities diff --git a/azext_iot/sdk/service/models/interface.py b/azext_iot/sdk/service/models/interface.py deleted file mode 100644 index b81ccd6cc..000000000 --- a/azext_iot/sdk/service/models/interface.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Interface(Model): - """Interface. - - :param name: Full name of digital twin interface. - :type name: str - :param properties: List of all properties in an interface. - :type properties: dict[str, ~service.models.Property] - """ - - _attribute_map = { - 'name': {'key': 'name', 'type': 'str'}, - 'properties': {'key': 'properties', 'type': '{Property}'}, - } - - def __init__(self, name=None, properties=None): - super(Interface, self).__init__() - self.name = name - self.properties = properties diff --git a/azext_iot/sdk/service/models/job_properties.py b/azext_iot/sdk/service/models/job_properties.py deleted file mode 100644 index 26b693563..000000000 --- a/azext_iot/sdk/service/models/job_properties.py +++ /dev/null @@ -1,89 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class JobProperties(Model): - """JobProperties. - - :param job_id: System generated. Ignored at creation. - :type job_id: str - :param start_time_utc: System generated. Ignored at creation. - :type start_time_utc: datetime - :param end_time_utc: System generated. Ignored at creation. - Represents the time the job stopped processing. - :type end_time_utc: datetime - :param type: Required. - The type of job to execute. Possible values include: 'unknown', 'export', - 'import', 'backup', 'readDeviceProperties', 'writeDeviceProperties', - 'updateDeviceConfiguration', 'rebootDevice', 'factoryResetDevice', - 'firmwareUpdate', 'scheduleDeviceMethod', 'scheduleUpdateTwin', - 'restoreFromBackup', 'failoverDataCopy' - :type type: str or ~service.models.enum - :param status: System generated. Ignored at creation. Possible values - include: 'unknown', 'enqueued', 'running', 'completed', 'failed', - 'cancelled', 'scheduled', 'queued' - :type status: str or ~service.models.enum - :param progress: System generated. Ignored at creation. - Represents the percentage of completion. - :type progress: int - :param input_blob_container_uri: URI containing SAS token to a blob - container that contains registry data to sync. - :type input_blob_container_uri: str - :param input_blob_name: The blob name to be used when importing from the - provided input blob container. - :type input_blob_name: str - :param output_blob_container_uri: URI containing SAS token to a blob - container. This is used to output the status of the job and the results. - :type output_blob_container_uri: str - :param output_blob_name: The name of the blob that will be created in the - provided output blob container. This blob will contain - the exported device registry information for the IoT Hub. - :type output_blob_name: str - :param exclude_keys_in_export: Optional for export jobs; ignored for other - jobs. Default: false. If false, authorization keys are included - in export output. Keys are exported as null otherwise. - :type exclude_keys_in_export: bool - :param failure_reason: System genereated. Ignored at creation. - If status == failure, this represents a string containing the reason. - :type failure_reason: str - """ - - _attribute_map = { - 'job_id': {'key': 'jobId', 'type': 'str'}, - 'start_time_utc': {'key': 'startTimeUtc', 'type': 'iso-8601'}, - 'end_time_utc': {'key': 'endTimeUtc', 'type': 'iso-8601'}, - 'type': {'key': 'type', 'type': 'str'}, - 'status': {'key': 'status', 'type': 'str'}, - 'progress': {'key': 'progress', 'type': 'int'}, - 'input_blob_container_uri': {'key': 'inputBlobContainerUri', 'type': 'str'}, - 'input_blob_name': {'key': 'inputBlobName', 'type': 'str'}, - 'output_blob_container_uri': {'key': 'outputBlobContainerUri', 'type': 'str'}, - 'output_blob_name': {'key': 'outputBlobName', 'type': 'str'}, - 'exclude_keys_in_export': {'key': 'excludeKeysInExport', 'type': 'bool'}, - 'failure_reason': {'key': 'failureReason', 'type': 'str'}, - } - - def __init__(self, job_id=None, start_time_utc=None, end_time_utc=None, type=None, status=None, progress=None, input_blob_container_uri=None, input_blob_name=None, output_blob_container_uri=None, output_blob_name=None, exclude_keys_in_export=None, failure_reason=None): - super(JobProperties, self).__init__() - self.job_id = job_id - self.start_time_utc = start_time_utc - self.end_time_utc = end_time_utc - self.type = type - self.status = status - self.progress = progress - self.input_blob_container_uri = input_blob_container_uri - self.input_blob_name = input_blob_name - self.output_blob_container_uri = output_blob_container_uri - self.output_blob_name = output_blob_name - self.exclude_keys_in_export = exclude_keys_in_export - self.failure_reason = failure_reason diff --git a/azext_iot/sdk/service/models/job_request.py b/azext_iot/sdk/service/models/job_request.py deleted file mode 100644 index 8d091c2ec..000000000 --- a/azext_iot/sdk/service/models/job_request.py +++ /dev/null @@ -1,62 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class JobRequest(Model): - """JobRequest. - - :param job_id: Job identifier - :type job_id: str - :param type: Required. - The type of job to execute. Possible values include: 'unknown', 'export', - 'import', 'backup', 'readDeviceProperties', 'writeDeviceProperties', - 'updateDeviceConfiguration', 'rebootDevice', 'factoryResetDevice', - 'firmwareUpdate', 'scheduleDeviceMethod', 'scheduleUpdateTwin', - 'restoreFromBackup', 'failoverDataCopy' - :type type: str or ~service.models.enum - :param cloud_to_device_method: Required if jobType is cloudToDeviceMethod. - The method type and parameters. - :type cloud_to_device_method: ~service.models.CloudToDeviceMethod - :param update_twin: - :type update_twin: ~service.models.Twin - :param query_condition: Required if jobType is updateTwin or - cloudToDeviceMethod. - Condition for device query to get devices to execute the job on - :type query_condition: str - :param start_time: ISO 8601 date time to start the job - :type start_time: datetime - :param max_execution_time_in_seconds: Max execution time in secounds (ttl - duration) - :type max_execution_time_in_seconds: long - """ - - # @digimaun - altered update_twin type from Twin to {object} - _attribute_map = { - 'job_id': {'key': 'jobId', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'str'}, - 'cloud_to_device_method': {'key': 'cloudToDeviceMethod', 'type': 'CloudToDeviceMethod'}, - 'update_twin': {'key': 'updateTwin', 'type': '{object}'}, - 'query_condition': {'key': 'queryCondition', 'type': 'str'}, - 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, - 'max_execution_time_in_seconds': {'key': 'maxExecutionTimeInSeconds', 'type': 'long'}, - } - - def __init__(self, job_id=None, type=None, cloud_to_device_method=None, update_twin=None, query_condition=None, start_time=None, max_execution_time_in_seconds=None): - super(JobRequest, self).__init__() - self.job_id = job_id - self.type = type - self.cloud_to_device_method = cloud_to_device_method - self.update_twin = update_twin - self.query_condition = query_condition - self.start_time = start_time - self.max_execution_time_in_seconds = max_execution_time_in_seconds diff --git a/azext_iot/sdk/service/models/job_response.py b/azext_iot/sdk/service/models/job_response.py deleted file mode 100644 index 68852dfc8..000000000 --- a/azext_iot/sdk/service/models/job_response.py +++ /dev/null @@ -1,87 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class JobResponse(Model): - """JobResponse. - - :param job_id: System generated. Ignored at creation. - :type job_id: str - :param query_condition: Device query condition. - :type query_condition: str - :param created_time: System generated. Ignored at creation. - :type created_time: datetime - :param start_time: Scheduled job start time in UTC. - :type start_time: datetime - :param end_time: System generated. Ignored at creation. - Represents the time the job stopped processing. - :type end_time: datetime - :param max_execution_time_in_seconds: Max execution time in secounds (ttl - duration) - :type max_execution_time_in_seconds: long - :param type: Required. - The type of job to execute. Possible values include: 'unknown', 'export', - 'import', 'backup', 'readDeviceProperties', 'writeDeviceProperties', - 'updateDeviceConfiguration', 'rebootDevice', 'factoryResetDevice', - 'firmwareUpdate', 'scheduleDeviceMethod', 'scheduleUpdateTwin', - 'restoreFromBackup', 'failoverDataCopy' - :type type: str or ~service.models.enum - :param cloud_to_device_method: Required if jobType is cloudToDeviceMethod. - The method type and parameters. - :type cloud_to_device_method: ~service.models.CloudToDeviceMethod - :param update_twin: - :type update_twin: ~service.models.Twin - :param status: System generated. Ignored at creation. Possible values - include: 'unknown', 'enqueued', 'running', 'completed', 'failed', - 'cancelled', 'scheduled', 'queued' - :type status: str or ~service.models.enum - :param failure_reason: System generated. Ignored at creation. - If status == failure, this represents a string containing the reason. - :type failure_reason: str - :param status_message: Status message for the job - :type status_message: str - :param device_job_statistics: Job details - :type device_job_statistics: ~service.models.DeviceJobStatistics - """ - - _attribute_map = { - 'job_id': {'key': 'jobId', 'type': 'str'}, - 'query_condition': {'key': 'queryCondition', 'type': 'str'}, - 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, - 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, - 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, - 'max_execution_time_in_seconds': {'key': 'maxExecutionTimeInSeconds', 'type': 'long'}, - 'type': {'key': 'type', 'type': 'str'}, - 'cloud_to_device_method': {'key': 'cloudToDeviceMethod', 'type': 'CloudToDeviceMethod'}, - 'update_twin': {'key': 'updateTwin', 'type': 'Twin'}, - 'status': {'key': 'status', 'type': 'str'}, - 'failure_reason': {'key': 'failureReason', 'type': 'str'}, - 'status_message': {'key': 'statusMessage', 'type': 'str'}, - 'device_job_statistics': {'key': 'deviceJobStatistics', 'type': 'DeviceJobStatistics'}, - } - - def __init__(self, job_id=None, query_condition=None, created_time=None, start_time=None, end_time=None, max_execution_time_in_seconds=None, type=None, cloud_to_device_method=None, update_twin=None, status=None, failure_reason=None, status_message=None, device_job_statistics=None): - super(JobResponse, self).__init__() - self.job_id = job_id - self.query_condition = query_condition - self.created_time = created_time - self.start_time = start_time - self.end_time = end_time - self.max_execution_time_in_seconds = max_execution_time_in_seconds - self.type = type - self.cloud_to_device_method = cloud_to_device_method - self.update_twin = update_twin - self.status = status - self.failure_reason = failure_reason - self.status_message = status_message - self.device_job_statistics = device_job_statistics diff --git a/azext_iot/sdk/service/models/module.py b/azext_iot/sdk/service/models/module.py deleted file mode 100644 index a32a455c5..000000000 --- a/azext_iot/sdk/service/models/module.py +++ /dev/null @@ -1,65 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Module(Model): - """Module identity on a device. - - :param module_id: - :type module_id: str - :param managed_by: - :type managed_by: str - :param device_id: - :type device_id: str - :param generation_id: - :type generation_id: str - :param etag: - :type etag: str - :param connection_state: Possible values include: 'Disconnected', - 'Connected' - :type connection_state: str or ~service.models.enum - :param connection_state_updated_time: - :type connection_state_updated_time: datetime - :param last_activity_time: - :type last_activity_time: datetime - :param cloud_to_device_message_count: - :type cloud_to_device_message_count: int - :param authentication: - :type authentication: ~service.models.AuthenticationMechanism - """ - - _attribute_map = { - 'module_id': {'key': 'moduleId', 'type': 'str'}, - 'managed_by': {'key': 'managedBy', 'type': 'str'}, - 'device_id': {'key': 'deviceId', 'type': 'str'}, - 'generation_id': {'key': 'generationId', 'type': 'str'}, - 'etag': {'key': 'etag', 'type': 'str'}, - 'connection_state': {'key': 'connectionState', 'type': 'str'}, - 'connection_state_updated_time': {'key': 'connectionStateUpdatedTime', 'type': 'iso-8601'}, - 'last_activity_time': {'key': 'lastActivityTime', 'type': 'iso-8601'}, - 'cloud_to_device_message_count': {'key': 'cloudToDeviceMessageCount', 'type': 'int'}, - 'authentication': {'key': 'authentication', 'type': 'AuthenticationMechanism'}, - } - - def __init__(self, module_id=None, managed_by=None, device_id=None, generation_id=None, etag=None, connection_state=None, connection_state_updated_time=None, last_activity_time=None, cloud_to_device_message_count=None, authentication=None): - super(Module, self).__init__() - self.module_id = module_id - self.managed_by = managed_by - self.device_id = device_id - self.generation_id = generation_id - self.etag = etag - self.connection_state = connection_state - self.connection_state_updated_time = connection_state_updated_time - self.last_activity_time = last_activity_time - self.cloud_to_device_message_count = cloud_to_device_message_count - self.authentication = authentication diff --git a/azext_iot/sdk/service/models/property.py b/azext_iot/sdk/service/models/property.py deleted file mode 100644 index c950c11db..000000000 --- a/azext_iot/sdk/service/models/property.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Property(Model): - """Property. - - :param reported: - :type reported: ~service.models.Reported - :param desired: - :type desired: ~service.models.Desired - """ - - _attribute_map = { - 'reported': {'key': 'reported', 'type': 'Reported'}, - 'desired': {'key': 'desired', 'type': 'Desired'}, - } - - def __init__(self, reported=None, desired=None): - super(Property, self).__init__() - self.reported = reported - self.desired = desired \ No newline at end of file diff --git a/azext_iot/sdk/service/models/property_container.py b/azext_iot/sdk/service/models/property_container.py deleted file mode 100644 index caa6ae42f..000000000 --- a/azext_iot/sdk/service/models/property_container.py +++ /dev/null @@ -1,39 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class PropertyContainer(Model): - """Represents Twin properties. - - :param desired: Used in conjunction with reported properties to - synchronize device configuration or condition. Desired properties can only - be set by the solution back end and can be read by the device app. The - device app can also be notified in real time of changes on the desired - properties. - :type desired: dict[str, object] - :param reported: Used in conjunction with desired properties to - synchronize device configuration or condition. Reported properties can - only be set by the device app and can be read and queried by the solution - back end. - :type reported: dict[str, object] - """ - - _attribute_map = { - 'desired': {'key': 'desired', 'type': '{object}'}, - 'reported': {'key': 'reported', 'type': '{object}'}, - } - - def __init__(self, desired=None, reported=None): - super(PropertyContainer, self).__init__() - self.desired = desired - self.reported = reported diff --git a/azext_iot/sdk/service/models/purge_message_queue_result.py b/azext_iot/sdk/service/models/purge_message_queue_result.py deleted file mode 100644 index 1b9896f7c..000000000 --- a/azext_iot/sdk/service/models/purge_message_queue_result.py +++ /dev/null @@ -1,36 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class PurgeMessageQueueResult(Model): - """Result of a device message queue purge operation. - - :param total_messages_purged: - :type total_messages_purged: int - :param device_id: The ID of the device whose messages are being purged. - :type device_id: str - :param module_id: The ID of the device whose messages are being purged. - :type module_id: str - """ - - _attribute_map = { - 'total_messages_purged': {'key': 'totalMessagesPurged', 'type': 'int'}, - 'device_id': {'key': 'deviceId', 'type': 'str'}, - 'module_id': {'key': 'moduleId', 'type': 'str'}, - } - - def __init__(self, total_messages_purged=None, device_id=None, module_id=None): - super(PurgeMessageQueueResult, self).__init__() - self.total_messages_purged = total_messages_purged - self.device_id = device_id - self.module_id = module_id diff --git a/azext_iot/sdk/service/models/query_result.py b/azext_iot/sdk/service/models/query_result.py deleted file mode 100644 index 77dfdc59d..000000000 --- a/azext_iot/sdk/service/models/query_result.py +++ /dev/null @@ -1,38 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class QueryResult(Model): - """The query result. - - :param type: The query result type. Possible values include: 'unknown', - 'twin', 'deviceJob', 'jobResponse', 'raw', 'enrollment', - 'enrollmentGroup', 'deviceRegistration' - :type type: str or ~service.models.enum - :param items: The query result items, as a collection. - :type items: list[object] - :param continuation_token: Request continuation token. - :type continuation_token: str - """ - - _attribute_map = { - 'type': {'key': 'type', 'type': 'str'}, - 'items': {'key': 'items', 'type': '[object]'}, - 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, - } - - def __init__(self, type=None, items=None, continuation_token=None): - super(QueryResult, self).__init__() - self.type = type - self.items = items - self.continuation_token = continuation_token diff --git a/azext_iot/sdk/service/models/query_specification.py b/azext_iot/sdk/service/models/query_specification.py deleted file mode 100644 index 67f7ad02c..000000000 --- a/azext_iot/sdk/service/models/query_specification.py +++ /dev/null @@ -1,28 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class QuerySpecification(Model): - """A Json query request. - - :param query: The query. - :type query: str - """ - - _attribute_map = { - 'query': {'key': 'query', 'type': 'str'}, - } - - def __init__(self, query=None): - super(QuerySpecification, self).__init__() - self.query = query diff --git a/azext_iot/sdk/service/models/registry_statistics.py b/azext_iot/sdk/service/models/registry_statistics.py deleted file mode 100644 index f917cd871..000000000 --- a/azext_iot/sdk/service/models/registry_statistics.py +++ /dev/null @@ -1,36 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class RegistryStatistics(Model): - """RegistryStatistics. - - :param total_device_count: - :type total_device_count: long - :param enabled_device_count: - :type enabled_device_count: long - :param disabled_device_count: - :type disabled_device_count: long - """ - - _attribute_map = { - 'total_device_count': {'key': 'totalDeviceCount', 'type': 'long'}, - 'enabled_device_count': {'key': 'enabledDeviceCount', 'type': 'long'}, - 'disabled_device_count': {'key': 'disabledDeviceCount', 'type': 'long'}, - } - - def __init__(self, total_device_count=None, enabled_device_count=None, disabled_device_count=None): - super(RegistryStatistics, self).__init__() - self.total_device_count = total_device_count - self.enabled_device_count = enabled_device_count - self.disabled_device_count = disabled_device_count diff --git a/azext_iot/sdk/service/models/reported.py b/azext_iot/sdk/service/models/reported.py deleted file mode 100644 index 1dc58b366..000000000 --- a/azext_iot/sdk/service/models/reported.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Reported(Model): - """Reported. - - :param value: The current interface property value in a digitalTwin. - :type value: object - :param desired_state: - :type desired_state: ~service.models.DesiredState - """ - - _attribute_map = { - 'value': {'key': 'value', 'type': 'object'}, - 'desired_state': {'key': 'desiredState', 'type': 'DesiredState'}, - } - - def __init__(self, value=None, desired_state=None): - super(Reported, self).__init__() - self.value = value - self.desired_state = desired_state diff --git a/azext_iot/sdk/service/models/service_statistics.py b/azext_iot/sdk/service/models/service_statistics.py deleted file mode 100644 index 9c3ee8cf3..000000000 --- a/azext_iot/sdk/service/models/service_statistics.py +++ /dev/null @@ -1,28 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ServiceStatistics(Model): - """ServiceStatistics. - - :param connected_device_count: - :type connected_device_count: long - """ - - _attribute_map = { - 'connected_device_count': {'key': 'connectedDeviceCount', 'type': 'long'}, - } - - def __init__(self, connected_device_count=None): - super(ServiceStatistics, self).__init__() - self.connected_device_count = connected_device_count diff --git a/azext_iot/sdk/service/models/symmetric_key.py b/azext_iot/sdk/service/models/symmetric_key.py deleted file mode 100644 index 6820dad37..000000000 --- a/azext_iot/sdk/service/models/symmetric_key.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class SymmetricKey(Model): - """SymmetricKey. - - :param primary_key: - :type primary_key: str - :param secondary_key: - :type secondary_key: str - """ - - _attribute_map = { - 'primary_key': {'key': 'primaryKey', 'type': 'str'}, - 'secondary_key': {'key': 'secondaryKey', 'type': 'str'}, - } - - def __init__(self, primary_key=None, secondary_key=None): - super(SymmetricKey, self).__init__() - self.primary_key = primary_key - self.secondary_key = secondary_key diff --git a/azext_iot/sdk/service/models/twin.py b/azext_iot/sdk/service/models/twin.py deleted file mode 100644 index dfffb8723..000000000 --- a/azext_iot/sdk/service/models/twin.py +++ /dev/null @@ -1,107 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Twin(Model): - """Twin Representation. - - :param device_id: The deviceId uniquely identifies the device in the IoT - hub's identity registry. A case-sensitive string (up to 128 char long) of - ASCII 7-bit alphanumeric chars + {'-', ':', '.', '+', '%', '_', '#', '*', - '?', '!', '(', ')', ',', '=', '@', ';', '$', '''}. - :type device_id: str - :param module_id: Gets and sets the Module Id. - :type module_id: str - :param tags: A JSON document read and written by the solution back end. - Tags are not visible to device apps. - :type tags: dict[str, object] - :param properties: Gets and sets the Twin properties. - :type properties: ~service.models.TwinProperties - :param etag: Twin's ETag - :type etag: str - :param version: Version for device twin, including tags and desired - properties - :type version: long - :param device_etag: Device's ETag - :type device_etag: str - :param status: Gets the corresponding Device's Status. Possible values - include: 'enabled', 'disabled' - :type status: str or ~service.models.enum - :param status_reason: Reason, if any, for the corresponding Device to be - in specified Status - :type status_reason: str - :param status_update_time: Time when the corresponding Device's Status was - last updated - :type status_update_time: datetime - :param connection_state: Corresponding Device's ConnectionState. Possible - values include: 'Disconnected', 'Connected' - :type connection_state: str or ~service.models.enum - :param last_activity_time: The last time the device connected, received or - sent a message. In ISO8601 datetime format in UTC, for example, - 2015-01-28T16:24:48.789Z. This does not update if the device uses the - HTTP/1 protocol to perform messaging operations. - :type last_activity_time: datetime - :param cloud_to_device_message_count: Number of messages sent to the - corresponding Device from the Cloud - :type cloud_to_device_message_count: int - :param authentication_type: Corresponding Device's authentication type. - Possible values include: 'sas', 'selfSigned', 'certificateAuthority', - 'none' - :type authentication_type: str or ~service.models.enum - :param x509_thumbprint: Corresponding Device's X509 thumbprint - :type x509_thumbprint: ~service.models.X509Thumbprint - :param capabilities: - :type capabilities: ~service.models.DeviceCapabilities - :param device_scope: - :type device_scope: str - """ - - _attribute_map = { - 'device_id': {'key': 'deviceId', 'type': 'str'}, - 'module_id': {'key': 'moduleId', 'type': 'str'}, - 'tags': {'key': 'tags', 'type': '{object}'}, - 'properties': {'key': 'properties', 'type': 'TwinProperties'}, - 'etag': {'key': 'etag', 'type': 'str'}, - 'version': {'key': 'version', 'type': 'long'}, - 'device_etag': {'key': 'deviceEtag', 'type': 'str'}, - 'status': {'key': 'status', 'type': 'str'}, - 'status_reason': {'key': 'statusReason', 'type': 'str'}, - 'status_update_time': {'key': 'statusUpdateTime', 'type': 'iso-8601'}, - 'connection_state': {'key': 'connectionState', 'type': 'str'}, - 'last_activity_time': {'key': 'lastActivityTime', 'type': 'iso-8601'}, - 'cloud_to_device_message_count': {'key': 'cloudToDeviceMessageCount', 'type': 'int'}, - 'authentication_type': {'key': 'authenticationType', 'type': 'str'}, - 'x509_thumbprint': {'key': 'x509Thumbprint', 'type': 'X509Thumbprint'}, - 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, - 'device_scope': {'key': 'deviceScope', 'type': 'str'}, - } - - def __init__(self, device_id=None, module_id=None, tags=None, properties=None, etag=None, version=None, device_etag=None, status=None, status_reason=None, status_update_time=None, connection_state=None, last_activity_time=None, cloud_to_device_message_count=None, authentication_type=None, x509_thumbprint=None, capabilities=None, device_scope=None): - super(Twin, self).__init__() - self.device_id = device_id - self.module_id = module_id - self.tags = tags - self.properties = properties - self.etag = etag - self.version = version - self.device_etag = device_etag - self.status = status - self.status_reason = status_reason - self.status_update_time = status_update_time - self.connection_state = connection_state - self.last_activity_time = last_activity_time - self.cloud_to_device_message_count = cloud_to_device_message_count - self.authentication_type = authentication_type - self.x509_thumbprint = x509_thumbprint - self.capabilities = capabilities - self.device_scope = device_scope diff --git a/azext_iot/sdk/service/models/twin_properties.py b/azext_iot/sdk/service/models/twin_properties.py deleted file mode 100644 index fc8184ac1..000000000 --- a/azext_iot/sdk/service/models/twin_properties.py +++ /dev/null @@ -1,39 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class TwinProperties(Model): - """Represents Twin properties. - - :param desired: Used in conjunction with reported properties to - synchronize device configuration or condition. Desired properties can only - be set by the solution back end and can be read by the device app. The - device app can also be notified in real time of changes on the desired - properties. - :type desired: dict[str, object] - :param reported: Used in conjunction with desired properties to - synchronize device configuration or condition. Reported properties can - only be set by the device app and can be read and queried by the solution - back end. - :type reported: dict[str, object] - """ - - _attribute_map = { - 'desired': {'key': 'desired', 'type': '{object}'}, - 'reported': {'key': 'reported', 'type': '{object}'}, - } - - def __init__(self, desired=None, reported=None): - super(TwinProperties, self).__init__() - self.desired = desired - self.reported = reported diff --git a/azext_iot/sdk/service/models/x509_thumbprint.py b/azext_iot/sdk/service/models/x509_thumbprint.py deleted file mode 100644 index 43e6360ba..000000000 --- a/azext_iot/sdk/service/models/x509_thumbprint.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class X509Thumbprint(Model): - """X509Thumbprint. - - :param primary_thumbprint: - :type primary_thumbprint: str - :param secondary_thumbprint: - :type secondary_thumbprint: str - """ - - _attribute_map = { - 'primary_thumbprint': {'key': 'primaryThumbprint', 'type': 'str'}, - 'secondary_thumbprint': {'key': 'secondaryThumbprint', 'type': 'str'}, - } - - def __init__(self, primary_thumbprint=None, secondary_thumbprint=None): - super(X509Thumbprint, self).__init__() - self.primary_thumbprint = primary_thumbprint - self.secondary_thumbprint = secondary_thumbprint diff --git a/azext_iot/sdk/service/version.py b/azext_iot/sdk/service/version.py deleted file mode 100644 index 729e580c0..000000000 --- a/azext_iot/sdk/service/version.py +++ /dev/null @@ -1,12 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -VERSION = "2019-07-01-preview" diff --git a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py index 7f43a97c8..8d9a9606c 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py @@ -285,7 +285,6 @@ def test_config_create_edge( assert "{}/configurations/{}?".format(hub_name, config_id.lower()) in url assert method == "PUT" assert body["id"] == config_id.lower() - assert body["contentType"] == "assignment" assert body.get("targetCondition") == target_condition assert body.get("priority") == priority assert body.get("labels") == evaluate_literal(labels, dict) @@ -418,7 +417,6 @@ def test_config_create_adm( assert "{}/configurations/{}?".format(hub_name, config_id.lower()) in url assert method == "PUT" assert body["id"] == config_id.lower() - assert body["contentType"] == "assignment" assert body.get("targetCondition") == target_condition assert body.get("priority") == priority assert body.get("labels") == evaluate_literal(labels, dict) @@ -632,7 +630,6 @@ def test_config_update(self, fixture_cmd, serviceclient, sample_config_show): assert headers["If-Match"] == '"{}"'.format(sample_config_show["etag"]) assert body["id"] == sample_config_show["id"] - assert body["contentType"] == "assignment" assert body.get("metrics") == sample_config_show.get("metrics") assert body.get("targetCondition") == sample_config_show.get("targetCondition") assert body.get("priority") == sample_config_show.get("priority") diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/test_iot_central_unit.py index fa6b6529a..5e51e9758 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/test_iot_central_unit.py @@ -7,13 +7,13 @@ import mock import pytest import json +import responses import ast from datetime import datetime from knack.util import CLIError from azure.cli.core.mock import DummyCli from azext_iot.central import commands_device_twin from azext_iot.central import commands_monitor -from azext_iot.common.shared import SdkType from azext_iot.central.providers import ( CentralDeviceProvider, CentralDeviceTemplateProvider, @@ -28,7 +28,7 @@ device_id = "mydevice" app_id = "myapp" -device_twin_result = "{device twin result}" +device_twin_result = {"deviceId": "{}".format(device_id)} resource = "shared_resource" @@ -40,17 +40,6 @@ def fixture_cmd(mocker): return cmd -@pytest.fixture() -def fixture_bind_sdk(mocker): - class mock_service_sdk: - def get_twin(self, device_id): - return device_twin_result - - mock = mocker.patch("azext_iot.central.providers.devicetwin_provider._bind_sdk") - mock.return_value = (mock_service_sdk(), None) - return mock - - @pytest.fixture() def fixture_requests_post(mocker): class MockJsonObject: @@ -135,18 +124,24 @@ class Cmd: class TestDeviceTwinShow: + @pytest.fixture + def service_client(self, mocked_response, fixture_cmd, fixture_get_iot_central_tokens): + mocked_response.add( + method=responses.GET, + url="https://{}/twins/{}".format(resource, device_id), + body=json.dumps(device_twin_result), + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + def test_device_twin_show_calls_get_twin( - self, fixture_bind_sdk, fixture_cmd, fixture_get_iot_central_tokens + self, service_client ): result = commands_device_twin.device_twin_show(fixture_cmd, device_id, app_id) - - # Ensure get_twin is called and result is returned - assert result is device_twin_result - - # Ensure _bind_sdk is called with correct parameters - assert fixture_bind_sdk.called is True - args = fixture_bind_sdk.call_args - assert args[0] == ({"entity": resource}, SdkType.service_sdk) + assert result == device_twin_result class TestMonitorEvents: diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index e29168061..0297c0b52 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -719,9 +719,7 @@ def test_hub_device_twins(self): # Region specific test if self.region not in ["West US 2", "North Europe", "Southeast Asia"]: - warnings.warn( - "Skipping distributed-tracing tests. IoT Hub not in supported region!" - ) + 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( diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index 23d7f523b..f1413778b 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -367,6 +367,10 @@ def _validate_directory(path): _validate_directory(EXTENSION_ROOT) + invalid_directories = [] for directory in directory_structure: if directory_structure[directory] is None: - pytest.fail("Directory: '{}' missing __init__.py".format(directory)) + invalid_directories.append("Directory: '{}' missing __init__.py".format(directory)) + + if invalid_directories: + pytest.fail(", ".join(directory)) From edd32c7f6b9af4f29c594ca738c81baa78d48371 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 7 Aug 2020 16:47:31 -0700 Subject: [PATCH 085/179] Update CI service connection [skip ci] --- .azure-devops/create-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index 4a6ee7a71..d39db27bb 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -96,7 +96,7 @@ jobs: - task: GitHubRelease@0 inputs: - gitHubConnection: GitHubP + gitHubConnection: AzIoTCLIGitHub repositoryName: $(Build.Repository.Name) action: 'create' target: '$(Build.SourceVersion)' From 2fadc92109e25d91fff781eb392717a122f3372c Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Mon, 10 Aug 2020 17:10:22 -0700 Subject: [PATCH 086/179] Fix lack of arg_type for new three state flag arg. --- azext_iot/_params.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 72570f31d..7397c358c 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -271,8 +271,9 @@ def load_arguments(self, _): ) context.argument( "default_eventhub", + arg_type=get_three_state_flag(), options_list=["--default-eventhub", "--eh"], - help="To get eventhub compatible connection-string for the IoT Hub\'s default eventhub." + help="Flag indicating the connection string returned is for the default EventHub endpoint. Default: false" ) with self.argument_context("iot hub job") as context: From e2fdd696f1ec0a49edb066ee40fa778781dd4190 Mon Sep 17 00:00:00 2001 From: Paul Montgomery Date: Tue, 11 Aug 2020 16:18:25 -0700 Subject: [PATCH 087/179] AICS (#232) Co-authored-by: Ryan Kelly --- azext_iot/__init__.py | 4 + azext_iot/product/__init__.py | 9 + azext_iot/product/_help.py | 36 + azext_iot/product/command_map.py | 28 + azext_iot/product/command_requirements.py | 13 + azext_iot/product/params.py | 39 + azext_iot/product/providers/__init__.py | 32 + azext_iot/product/providers/aics.py | 122 ++ azext_iot/product/providers/auth.py | 57 + azext_iot/product/shared.py | 47 + azext_iot/product/test/__init__.py | 9 + azext_iot/product/test/_help.py | 190 +++ azext_iot/product/test/command_map.py | 40 + azext_iot/product/test/command_test_cases.py | 26 + azext_iot/product/test/command_test_runs.py | 41 + azext_iot/product/test/command_test_tasks.py | 69 ++ azext_iot/product/test/command_tests.py | 296 +++++ azext_iot/product/test/params.py | 165 +++ azext_iot/sdk/product/__init__.py | 18 + azext_iot/sdk/product/aicsapi.py | 1032 +++++++++++++++++ azext_iot/sdk/product/models/__init__.py | 187 +++ azext_iot/sdk/product/models/array_schema.py | 58 + .../sdk/product/models/array_schema_py3.py | 58 + azext_iot/sdk/product/models/badge_info.py | 33 + .../sdk/product/models/badge_info_py3.py | 33 + azext_iot/sdk/product/models/c2_dtest.py | 40 + azext_iot/sdk/product/models/c2_dtest_py3.py | 40 + ...rieve_model_reposistory_sas_token_error.py | 36 + ...e_model_reposistory_sas_token_error_py3.py | 36 + .../product/models/certification_task_log.py | 32 + .../models/certification_task_log_py3.py | 32 + azext_iot/sdk/product/models/command_model.py | 61 + .../sdk/product/models/command_model_py3.py | 61 + azext_iot/sdk/product/models/command_test.py | 52 + .../sdk/product/models/command_test_py3.py | 52 + azext_iot/sdk/product/models/d2_ctest.py | 44 + azext_iot/sdk/product/models/d2_ctest_py3.py | 44 + ..._certification_provisioning_requirement.py | 28 + ...tification_provisioning_requirement_py3.py | 28 + .../device_certification_requirement.py | 34 + .../device_certification_requirement_py3.py | 34 + azext_iot/sdk/product/models/device_test.py | 59 + .../models/device_test_not_exist_error.py | 36 + .../models/device_test_not_exist_error_py3.py | 36 + .../sdk/product/models/device_test_py3.py | 59 + .../models/device_test_search_options.py | 36 + .../models/device_test_search_options_py3.py | 36 + .../models/device_test_search_result.py | 46 + .../models/device_test_search_result_py3.py | 46 + .../sdk/product/models/device_test_task.py | 53 + .../product/models/device_test_task_py3.py | 53 + .../sdk/product/models/device_twin_test.py | 44 + .../product/models/device_twin_test_py3.py | 44 + .../digital_twin_validation_task_result.py | 74 ++ ...digital_twin_validation_task_result_py3.py | 74 ++ .../sdk/product/models/direct_method_test.py | 44 + .../product/models/direct_method_test_py3.py | 44 + .../edge_device_validation_task_result.py | 66 ++ .../edge_device_validation_task_result_py3.py | 66 ++ azext_iot/sdk/product/models/enum_schema.py | 64 + .../sdk/product/models/enum_schema_py3.py | 64 + azext_iot/sdk/product/models/enum_value.py | 52 + .../sdk/product/models/enum_value_py3.py | 52 + .../existing_task_running_conflict_error.py | 36 + ...xisting_task_running_conflict_error_py3.py | 36 + .../models/fail_to_queue_task_error.py | 36 + .../models/fail_to_queue_task_error_py3.py | 36 + .../models/interface_definition_snapshot.py | 37 + .../interface_definition_snapshot_py3.py | 37 + .../sdk/product/models/interface_test.py | 61 + .../sdk/product/models/interface_test_py3.py | 61 + ...evice_certification_badge_configuration.py | 29 + ...e_certification_badge_configuration_py3.py | 29 + .../iot_device_certification_badge_result.py | 42 + ...t_device_certification_badge_result_py3.py | 42 + ...t_device_certification_badge_test_cases.py | 45 + ...vice_certification_badge_test_cases_py3.py | 45 + .../iot_device_validation_task_result.py | 66 ++ .../iot_device_validation_task_result_py3.py | 66 ++ ...tible_certification_badge_configuration.py | 29 + ...e_certification_badge_configuration_py3.py | 29 + ...e_compatible_certification_badge_result.py | 42 + ...mpatible_certification_badge_result_py3.py | 42 + ...mpatible_certification_badge_test_cases.py | 29 + ...ible_certification_badge_test_cases_py3.py | 29 + azext_iot/sdk/product/models/map_schema.py | 62 + .../sdk/product/models/map_schema_py3.py | 62 + .../models/model_definition_snapshot.py | 42 + .../models/model_definition_snapshot_py3.py | 42 + .../models/model_resolution_failure_error.py | 36 + .../model_resolution_failure_error_py3.py | 36 + .../product/models/model_resolution_source.py | 33 + .../models/model_resolution_source_py3.py | 33 + .../sdk/product/models/new_task_payload.py | 29 + .../product/models/new_task_payload_py3.py | 29 + azext_iot/sdk/product/models/object_schema.py | 58 + .../sdk/product/models/object_schema_py3.py | 58 + .../pnp_certification_badge_configuration.py | 37 + ...p_certification_badge_configuration_py3.py | 37 + .../models/pnp_certification_badge_result.py | 53 + .../pnp_certification_badge_result_py3.py | 53 + .../pnp_certification_badge_test_cases.py | 37 + .../pnp_certification_badge_test_cases_py3.py | 37 + .../models/pre_validation_task_result.py | 57 + .../models/pre_validation_task_result_py3.py | 57 + .../sdk/product/models/property_model.py | 64 + .../sdk/product/models/property_model_py3.py | 64 + azext_iot/sdk/product/models/property_test.py | 48 + .../sdk/product/models/property_test_py3.py | 48 + .../models/provisioning_configuration.py | 60 + .../models/provisioning_configuration_py3.py | 60 + azext_iot/sdk/product/models/schema_field.py | 52 + .../sdk/product/models/schema_field_py3.py | 52 + .../models/symmetric_key_enrollment.py | 40 + .../models/symmetric_key_enrollment_py3.py | 40 + azext_iot/sdk/product/models/system_error.py | 36 + .../sdk/product/models/system_error_py3.py | 36 + .../sdk/product/models/telemetry_model.py | 60 + .../sdk/product/models/telemetry_model_py3.py | 60 + .../sdk/product/models/telemetry_test.py | 48 + .../sdk/product/models/telemetry_test_py3.py | 48 + azext_iot/sdk/product/models/test_cases.py | 28 + .../models/test_cases_not_exist_error.py | 36 + .../models/test_cases_not_exist_error_py3.py | 36 + .../sdk/product/models/test_cases_py3.py | 28 + azext_iot/sdk/product/models/test_run.py | 52 + .../models/test_run_not_exist_error.py | 36 + .../models/test_run_not_exist_error_py3.py | 36 + azext_iot/sdk/product/models/test_run_py3.py | 52 + .../sdk/product/models/tpm_enrollment.py | 40 + .../sdk/product/models/tpm_enrollment_py3.py | 40 + .../models/validation_problem_details.py | 60 + .../models/validation_problem_details_py3.py | 60 + .../sdk/product/models/x509_enrollment.py | 44 + .../sdk/product/models/x509_enrollment_py3.py | 44 + azext_iot/sdk/product/version.py | 13 + azext_iot/tests/conftest.py | 9 + azext_iot/tests/product/__init__.py | 16 + azext_iot/tests/product/test_aics_e2e_int.py | 171 +++ .../test_command_requirement_list_int.py | 63 + .../test_command_test_case_list_int.py | 27 + .../test_command_test_case_update_unit.py | 53 + .../product/test_command_test_create_int.py | 38 + .../product/test_command_test_create_unit.py | 302 +++++ .../product/test_command_test_run_int.py | 65 ++ .../product/test_command_test_run_unit.py | 240 ++++ .../product/test_command_test_search_unit.py | 51 + .../product/test_command_test_show_int.py | 23 + .../product/test_command_test_update_int.py | 29 + .../product/test_command_test_update_unit.py | 411 +++++++ .../product/test_device_test_tasks_int.py | 82 ++ .../product/test_device_test_tasks_unit.py | 345 ++++++ 152 files changed, 9582 insertions(+) create mode 100644 azext_iot/product/__init__.py create mode 100644 azext_iot/product/_help.py create mode 100644 azext_iot/product/command_map.py create mode 100644 azext_iot/product/command_requirements.py create mode 100644 azext_iot/product/params.py create mode 100644 azext_iot/product/providers/__init__.py create mode 100644 azext_iot/product/providers/aics.py create mode 100644 azext_iot/product/providers/auth.py create mode 100644 azext_iot/product/shared.py create mode 100644 azext_iot/product/test/__init__.py create mode 100644 azext_iot/product/test/_help.py create mode 100644 azext_iot/product/test/command_map.py create mode 100644 azext_iot/product/test/command_test_cases.py create mode 100644 azext_iot/product/test/command_test_runs.py create mode 100644 azext_iot/product/test/command_test_tasks.py create mode 100644 azext_iot/product/test/command_tests.py create mode 100644 azext_iot/product/test/params.py create mode 100644 azext_iot/sdk/product/__init__.py create mode 100644 azext_iot/sdk/product/aicsapi.py create mode 100644 azext_iot/sdk/product/models/__init__.py create mode 100644 azext_iot/sdk/product/models/array_schema.py create mode 100644 azext_iot/sdk/product/models/array_schema_py3.py create mode 100644 azext_iot/sdk/product/models/badge_info.py create mode 100644 azext_iot/sdk/product/models/badge_info_py3.py create mode 100644 azext_iot/sdk/product/models/c2_dtest.py create mode 100644 azext_iot/sdk/product/models/c2_dtest_py3.py create mode 100644 azext_iot/sdk/product/models/cannot_retrieve_model_reposistory_sas_token_error.py create mode 100644 azext_iot/sdk/product/models/cannot_retrieve_model_reposistory_sas_token_error_py3.py create mode 100644 azext_iot/sdk/product/models/certification_task_log.py create mode 100644 azext_iot/sdk/product/models/certification_task_log_py3.py create mode 100644 azext_iot/sdk/product/models/command_model.py create mode 100644 azext_iot/sdk/product/models/command_model_py3.py create mode 100644 azext_iot/sdk/product/models/command_test.py create mode 100644 azext_iot/sdk/product/models/command_test_py3.py create mode 100644 azext_iot/sdk/product/models/d2_ctest.py create mode 100644 azext_iot/sdk/product/models/d2_ctest_py3.py create mode 100644 azext_iot/sdk/product/models/device_certification_provisioning_requirement.py create mode 100644 azext_iot/sdk/product/models/device_certification_provisioning_requirement_py3.py create mode 100644 azext_iot/sdk/product/models/device_certification_requirement.py create mode 100644 azext_iot/sdk/product/models/device_certification_requirement_py3.py create mode 100644 azext_iot/sdk/product/models/device_test.py create mode 100644 azext_iot/sdk/product/models/device_test_not_exist_error.py create mode 100644 azext_iot/sdk/product/models/device_test_not_exist_error_py3.py create mode 100644 azext_iot/sdk/product/models/device_test_py3.py create mode 100644 azext_iot/sdk/product/models/device_test_search_options.py create mode 100644 azext_iot/sdk/product/models/device_test_search_options_py3.py create mode 100644 azext_iot/sdk/product/models/device_test_search_result.py create mode 100644 azext_iot/sdk/product/models/device_test_search_result_py3.py create mode 100644 azext_iot/sdk/product/models/device_test_task.py create mode 100644 azext_iot/sdk/product/models/device_test_task_py3.py create mode 100644 azext_iot/sdk/product/models/device_twin_test.py create mode 100644 azext_iot/sdk/product/models/device_twin_test_py3.py create mode 100644 azext_iot/sdk/product/models/digital_twin_validation_task_result.py create mode 100644 azext_iot/sdk/product/models/digital_twin_validation_task_result_py3.py create mode 100644 azext_iot/sdk/product/models/direct_method_test.py create mode 100644 azext_iot/sdk/product/models/direct_method_test_py3.py create mode 100644 azext_iot/sdk/product/models/edge_device_validation_task_result.py create mode 100644 azext_iot/sdk/product/models/edge_device_validation_task_result_py3.py create mode 100644 azext_iot/sdk/product/models/enum_schema.py create mode 100644 azext_iot/sdk/product/models/enum_schema_py3.py create mode 100644 azext_iot/sdk/product/models/enum_value.py create mode 100644 azext_iot/sdk/product/models/enum_value_py3.py create mode 100644 azext_iot/sdk/product/models/existing_task_running_conflict_error.py create mode 100644 azext_iot/sdk/product/models/existing_task_running_conflict_error_py3.py create mode 100644 azext_iot/sdk/product/models/fail_to_queue_task_error.py create mode 100644 azext_iot/sdk/product/models/fail_to_queue_task_error_py3.py create mode 100644 azext_iot/sdk/product/models/interface_definition_snapshot.py create mode 100644 azext_iot/sdk/product/models/interface_definition_snapshot_py3.py create mode 100644 azext_iot/sdk/product/models/interface_test.py create mode 100644 azext_iot/sdk/product/models/interface_test_py3.py create mode 100644 azext_iot/sdk/product/models/iot_device_certification_badge_configuration.py create mode 100644 azext_iot/sdk/product/models/iot_device_certification_badge_configuration_py3.py create mode 100644 azext_iot/sdk/product/models/iot_device_certification_badge_result.py create mode 100644 azext_iot/sdk/product/models/iot_device_certification_badge_result_py3.py create mode 100644 azext_iot/sdk/product/models/iot_device_certification_badge_test_cases.py create mode 100644 azext_iot/sdk/product/models/iot_device_certification_badge_test_cases_py3.py create mode 100644 azext_iot/sdk/product/models/iot_device_validation_task_result.py create mode 100644 azext_iot/sdk/product/models/iot_device_validation_task_result_py3.py create mode 100644 azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_configuration.py create mode 100644 azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_configuration_py3.py create mode 100644 azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_result.py create mode 100644 azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_result_py3.py create mode 100644 azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_test_cases.py create mode 100644 azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_test_cases_py3.py create mode 100644 azext_iot/sdk/product/models/map_schema.py create mode 100644 azext_iot/sdk/product/models/map_schema_py3.py create mode 100644 azext_iot/sdk/product/models/model_definition_snapshot.py create mode 100644 azext_iot/sdk/product/models/model_definition_snapshot_py3.py create mode 100644 azext_iot/sdk/product/models/model_resolution_failure_error.py create mode 100644 azext_iot/sdk/product/models/model_resolution_failure_error_py3.py create mode 100644 azext_iot/sdk/product/models/model_resolution_source.py create mode 100644 azext_iot/sdk/product/models/model_resolution_source_py3.py create mode 100644 azext_iot/sdk/product/models/new_task_payload.py create mode 100644 azext_iot/sdk/product/models/new_task_payload_py3.py create mode 100644 azext_iot/sdk/product/models/object_schema.py create mode 100644 azext_iot/sdk/product/models/object_schema_py3.py create mode 100644 azext_iot/sdk/product/models/pnp_certification_badge_configuration.py create mode 100644 azext_iot/sdk/product/models/pnp_certification_badge_configuration_py3.py create mode 100644 azext_iot/sdk/product/models/pnp_certification_badge_result.py create mode 100644 azext_iot/sdk/product/models/pnp_certification_badge_result_py3.py create mode 100644 azext_iot/sdk/product/models/pnp_certification_badge_test_cases.py create mode 100644 azext_iot/sdk/product/models/pnp_certification_badge_test_cases_py3.py create mode 100644 azext_iot/sdk/product/models/pre_validation_task_result.py create mode 100644 azext_iot/sdk/product/models/pre_validation_task_result_py3.py create mode 100644 azext_iot/sdk/product/models/property_model.py create mode 100644 azext_iot/sdk/product/models/property_model_py3.py create mode 100644 azext_iot/sdk/product/models/property_test.py create mode 100644 azext_iot/sdk/product/models/property_test_py3.py create mode 100644 azext_iot/sdk/product/models/provisioning_configuration.py create mode 100644 azext_iot/sdk/product/models/provisioning_configuration_py3.py create mode 100644 azext_iot/sdk/product/models/schema_field.py create mode 100644 azext_iot/sdk/product/models/schema_field_py3.py create mode 100644 azext_iot/sdk/product/models/symmetric_key_enrollment.py create mode 100644 azext_iot/sdk/product/models/symmetric_key_enrollment_py3.py create mode 100644 azext_iot/sdk/product/models/system_error.py create mode 100644 azext_iot/sdk/product/models/system_error_py3.py create mode 100644 azext_iot/sdk/product/models/telemetry_model.py create mode 100644 azext_iot/sdk/product/models/telemetry_model_py3.py create mode 100644 azext_iot/sdk/product/models/telemetry_test.py create mode 100644 azext_iot/sdk/product/models/telemetry_test_py3.py create mode 100644 azext_iot/sdk/product/models/test_cases.py create mode 100644 azext_iot/sdk/product/models/test_cases_not_exist_error.py create mode 100644 azext_iot/sdk/product/models/test_cases_not_exist_error_py3.py create mode 100644 azext_iot/sdk/product/models/test_cases_py3.py create mode 100644 azext_iot/sdk/product/models/test_run.py create mode 100644 azext_iot/sdk/product/models/test_run_not_exist_error.py create mode 100644 azext_iot/sdk/product/models/test_run_not_exist_error_py3.py create mode 100644 azext_iot/sdk/product/models/test_run_py3.py create mode 100644 azext_iot/sdk/product/models/tpm_enrollment.py create mode 100644 azext_iot/sdk/product/models/tpm_enrollment_py3.py create mode 100644 azext_iot/sdk/product/models/validation_problem_details.py create mode 100644 azext_iot/sdk/product/models/validation_problem_details_py3.py create mode 100644 azext_iot/sdk/product/models/x509_enrollment.py create mode 100644 azext_iot/sdk/product/models/x509_enrollment_py3.py create mode 100644 azext_iot/sdk/product/version.py create mode 100644 azext_iot/tests/product/__init__.py create mode 100644 azext_iot/tests/product/test_aics_e2e_int.py create mode 100644 azext_iot/tests/product/test_command_requirement_list_int.py create mode 100644 azext_iot/tests/product/test_command_test_case_list_int.py create mode 100644 azext_iot/tests/product/test_command_test_case_update_unit.py create mode 100644 azext_iot/tests/product/test_command_test_create_int.py create mode 100644 azext_iot/tests/product/test_command_test_create_unit.py create mode 100644 azext_iot/tests/product/test_command_test_run_int.py create mode 100644 azext_iot/tests/product/test_command_test_run_unit.py create mode 100644 azext_iot/tests/product/test_command_test_search_unit.py create mode 100644 azext_iot/tests/product/test_command_test_show_int.py create mode 100644 azext_iot/tests/product/test_command_test_update_int.py create mode 100644 azext_iot/tests/product/test_command_test_update_unit.py create mode 100644 azext_iot/tests/product/test_device_test_tasks_int.py create mode 100644 azext_iot/tests/product/test_device_test_tasks_unit.py diff --git a/azext_iot/__init__.py b/azext_iot/__init__.py index 557a6f753..70ea02e6e 100644 --- a/azext_iot/__init__.py +++ b/azext_iot/__init__.py @@ -9,6 +9,7 @@ from azext_iot._factory import iot_service_provisioning_factory from azext_iot.constants import VERSION import azext_iot._help # noqa: F401 +from azext_iot.product.command_map import load_product_commands iothub_ops = CliCommandType(operations_tmpl="azext_iot.operations.hub#{}") @@ -33,6 +34,7 @@ def load_command_table(self, args): load_iothub_commands(self, args) load_central_commands(self, args) load_digitaltwins_commands(self, args) + load_product_commands(self, args) load_pnp_commands(self, args) return self.command_table @@ -42,12 +44,14 @@ def load_arguments(self, command): from azext_iot.iothub.params import load_iothub_arguments from azext_iot.central.params import load_central_arguments from azext_iot.digitaltwins.params import load_digitaltwins_arguments + from azext_iot.product.params import load_product_params from azext_iot.pnp.params import load_pnp_arguments load_arguments(self, command) load_iothub_arguments(self, command) load_central_arguments(self, command) load_digitaltwins_arguments(self, command) + load_product_params(self, command) load_pnp_arguments(self, command) diff --git a/azext_iot/product/__init__.py b/azext_iot/product/__init__.py new file mode 100644 index 000000000..025118e43 --- /dev/null +++ b/azext_iot/product/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.product._help import load_help + +load_help() diff --git a/azext_iot/product/_help.py b/azext_iot/product/_help.py new file mode 100644 index 000000000..50e941768 --- /dev/null +++ b/azext_iot/product/_help.py @@ -0,0 +1,36 @@ +# 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. +# -------------------------------------------------------------------------------------------- +""" +Help definitions for Product Certification commands. +""" + +from knack.help_files import helps + + +def load_help(): + helps[ + "iot product" + ] = """ + type: group + short-summary: Manage device testing for product certification + """ + # certification requirements + helps[ + "iot product requirement" + ] = """ + type: group + short-summary: Manage product certification requirements + """ + helps[ + "iot product requirement list" + ] = """ + type: command + short-summary: Discover information about provisioning attestation methods that are supported for each badge type + examples: + - name: Basic usage + text: > + az iot product requirement list + """ diff --git a/azext_iot/product/command_map.py b/azext_iot/product/command_map.py new file mode 100644 index 000000000..25dba78b7 --- /dev/null +++ b/azext_iot/product/command_map.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +""" +Load CLI commands +""" +from azure.cli.core.commands import CliCommandType + +product_ops = CliCommandType( + operations_tmpl="azext_iot.product.command_product#{}" +) + +requirements_ops = CliCommandType( + operations_tmpl="azext_iot.product.command_requirements#{}" +) + + +def load_product_commands(self, _): + with self.command_group( + "iot product requirement", command_type=requirements_ops + ) as g: + g.command("list", "list") + + from azext_iot.product.test.command_map import load_product_test_commands + load_product_test_commands(self, _) diff --git a/azext_iot/product/command_requirements.py b/azext_iot/product/command_requirements.py new file mode 100644 index 000000000..0a486e28a --- /dev/null +++ b/azext_iot/product/command_requirements.py @@ -0,0 +1,13 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.product.shared import BadgeType +from azext_iot.product.providers.aics import AICSProvider + + +def list(cmd, badge_type=BadgeType.IotDevice.value, base_url=None): + ap = AICSProvider(cmd, base_url) + return ap.list_requirements(badge_type=badge_type) diff --git a/azext_iot/product/params.py b/azext_iot/product/params.py new file mode 100644 index 000000000..e2d32bf5e --- /dev/null +++ b/azext_iot/product/params.py @@ -0,0 +1,39 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +""" +CLI parameter definitions. +""" + +from azure.cli.core.commands.parameters import get_enum_type +from azext_iot.product.shared import BadgeType + + +def load_product_params(self, _): + with self.argument_context("iot product") as c: + c.argument( + "test_id", + options_list=["--test-id", "-t"], + help="The generated Id for the device certification test", + arg_group="IoT Device Certification", + ) + c.argument( + "badge_type", + options_list=["--badge-type", "--bt"], + help="The type of certification badge", + arg_group="IoT Device Certification", + arg_type=get_enum_type(BadgeType), + ) + c.argument( + "base_url", + options_list=["--base-url"], + help="Override certification service URL to allow testing in non-production environements.", + arg_group="Development Settings" + ) + + # load az iot product test parameters + from azext_iot.product.test.params import load_product_test_params + load_product_test_params(self, _) diff --git a/azext_iot/product/providers/__init__.py b/azext_iot/product/providers/__init__.py new file mode 100644 index 000000000..903f27ac3 --- /dev/null +++ b/azext_iot/product/providers/__init__.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.product.providers.auth import AICSAuthentication +from azext_iot.sdk.product import AICSAPI +from azext_iot.product.shared import BASE_URL +from azext_iot.constants import VERSION as cli_version, USER_AGENT + +__all__ = ["aics_service_factory", "AICSServiceProvider"] + + +def aics_service_factory(cmd, base_url): + creds = AICSAuthentication(cmd=cmd, base_url=base_url or BASE_URL) + api = AICSAPI(base_url=base_url or BASE_URL, credentials=creds) + + api.config.add_user_agent(USER_AGENT) + api.config.add_user_agent("azcli/aics/{}".format(cli_version)) + + return api + + +class AICSServiceProvider(object): + def __init__(self, cmd, base_url): + assert cmd + self.cmd = cmd + self.base_url = base_url + + def get_mgmt_sdk(self): + return aics_service_factory(self.cmd, self.base_url) diff --git a/azext_iot/product/providers/aics.py b/azext_iot/product/providers/aics.py new file mode 100644 index 000000000..ef49d5e1d --- /dev/null +++ b/azext_iot/product/providers/aics.py @@ -0,0 +1,122 @@ +# 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 functools import wraps +from azext_iot.product.providers import AICSServiceProvider +from azext_iot.product.shared import ( + TaskType, + BadgeType, +) +from msrestazure.azure_exceptions import CloudError +from azext_iot.common.utility import unpack_msrest_error +from knack.log import get_logger +from knack.util import CLIError + +logger = get_logger(__name__) + + +def process_cloud_error(func): + @wraps(func) + def catch_unpack_clouderror(*args, **kwargs): + """Process / unpack CloudError exceptions as CLIErrors""" + try: + return func(*args, **kwargs) + except CloudError as e: + return CLIError(unpack_msrest_error(e)) + + return catch_unpack_clouderror + + +class AICSProvider(AICSServiceProvider): + def __init__(self, cmd, base_url): + super(AICSProvider, self).__init__(cmd=cmd, base_url=base_url) + self.mgmt_sdk = self.get_mgmt_sdk() + + # Requirements + @process_cloud_error + def list_requirements(self, badge_type=BadgeType.IotDevice): + return self.mgmt_sdk.get_device_certification_requirements( + badge_type=badge_type + ) + + # Test Tasks + @process_cloud_error + def create_test_task( + self, test_id, task_type=TaskType.QueueTestRun, wait=False, poll_interval=3 + ): + return self.mgmt_sdk.create_device_test_task( + device_test_id=test_id, task_type=task_type + ) + + @process_cloud_error + def delete_test_task(self, test_id, task_id): + return self.mgmt_sdk.cancel_device_test_task( + task_id=task_id, device_test_id=test_id + ) + + @process_cloud_error + def show_test_task(self, test_id, task_id=None): + return self.mgmt_sdk.get_device_test_task( + task_id=task_id, device_test_id=test_id + ) + + @process_cloud_error + def show_running_test_task(self, test_id): + return self.mgmt_sdk.get_running_device_test_tasks(device_test_id=test_id) + + # Tests + @process_cloud_error + def show_test(self, test_id): + return self.mgmt_sdk.get_device_test( + device_test_id=test_id, raw=True + ).response.json() + + @process_cloud_error + def search_test(self, searchOptions): + return self.mgmt_sdk.search_device_test(body=searchOptions) + + @process_cloud_error + def update_test( + self, test_id, test_configuration, provisioning=False + ): + return self.mgmt_sdk.update_device_test( + device_test_id=test_id, + generate_provisioning_configuration=provisioning, + body=test_configuration, + raw=True, + ).response.json() + + @process_cloud_error + def create_test( + self, test_configuration, provisioning=True, + ): + return self.mgmt_sdk.create_device_test( + provisioning=provisioning, body=test_configuration + ) + + # Test runs + @process_cloud_error + def show_test_run(self, test_id, run_id): + return self.mgmt_sdk.get_test_run(test_run_id=run_id, device_test_id=test_id) + + @process_cloud_error + def show_test_run_latest(self, test_id): + return self.mgmt_sdk.get_latest_test_run(device_test_id=test_id) + + @process_cloud_error + def submit_test_run(self, test_id, run_id): + return self.mgmt_sdk.submit_test_run(test_run_id=run_id, device_test_id=test_id) + + # Test cases + @process_cloud_error + def show_test_cases(self, test_id): + return self.mgmt_sdk.get_test_cases(device_test_id=test_id) + + @process_cloud_error + def update_test_cases(self, test_id, patch): + return self.mgmt_sdk.update_test_cases( + device_test_id=test_id, certification_badge_test_cases=patch + ) diff --git a/azext_iot/product/providers/auth.py b/azext_iot/product/providers/auth.py new file mode 100644 index 000000000..d441fd712 --- /dev/null +++ b/azext_iot/product/providers/auth.py @@ -0,0 +1,57 @@ +# 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 AICSAuthentication(Authentication): + def __init__(self, cmd, base_url): + self.cmd = cmd + self.base_url = base_url + + 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(AICSAuthentication, 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() + parsed_token = { + "tokenType": creds[0], + "accessToken": creds[1], + "expiresOn": creds[2].get("expiresOn", "N/A"), + "tenant": tenant, + "subscription": subscription, + } + return "{} {}".format(parsed_token["tokenType"], parsed_token["accessToken"]) diff --git a/azext_iot/product/shared.py b/azext_iot/product/shared.py new file mode 100644 index 000000000..603182820 --- /dev/null +++ b/azext_iot/product/shared.py @@ -0,0 +1,47 @@ +# 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 enum import Enum + + +class TaskType(Enum): + QueueTestRun = "QueueTestRun" + GenerateTestCases = "GenerateTestCases" + + +class BadgeType(Enum): + IotDevice = "IotDevice" + Pnp = "Pnp" + IotEdgeCompatible = "IotEdgeCompatible" + + +class AttestationType(Enum): + symmetricKey = "SymmetricKey" + tpm = "TPM" + x509 = "X509" + connectionString = "ConnectionString" + + +class DeviceType(Enum): + FinishedProduct = "FinishedProduct" + DevKit = "DevKit" + + +class DeviceTestTaskStatus(Enum): + queued = "Queued" + started = "Started" + running = "Running" + completed = "Completed" + failed = "Failed" + cancelled = "Cancelled" + + +class ValidationType(Enum): + test = "Test" + certification = "Certification" + + +BASE_URL = "https://certify.azureiotsolutions.com" diff --git a/azext_iot/product/test/__init__.py b/azext_iot/product/test/__init__.py new file mode 100644 index 000000000..5d773cc29 --- /dev/null +++ b/azext_iot/product/test/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_iot.product.test._help import load_help + +load_help() diff --git a/azext_iot/product/test/_help.py b/azext_iot/product/test/_help.py new file mode 100644 index 000000000..488faa9d0 --- /dev/null +++ b/azext_iot/product/test/_help.py @@ -0,0 +1,190 @@ +# 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. +# -------------------------------------------------------------------------------------------- +""" +Help definitions for Product Certification Testing commands. +""" + +from knack.help_files import helps + + +def load_help(): + # product tests + helps[ + "iot product test" + ] = """ + type: group + short-summary: Manage device tests for product certification + """ + helps[ + "iot product test create" + ] = """ + type: command + short-summary: Create a new product test for product certification + examples: + - name: Basic usage + text: > + az iot product test create --configuration-file {configuration_file} + - name: Do not have service create provisioning configuration + text: > + az iot product test create --configuration-file {configuration_file} --skip-provisioning + - name: Creating test with symmetric key attestation + text: > + az iot product test create --attestation-type SymmetricKey --device-type {device_type} + - name: Creating test with TPM attestation + text: > + az iot product test create --attestation-type TPM --device-type {device_type} --endorsement-key {endorsement_key} + - name: Creating test with x509 attestation + text: > + az iot product test create --attestation-type x509 --device-type {device_type} --certificate-path {certificate_path} + - name: Creating test for Edge module + text: > + az iot product test create --attestation-type ConnectionString --device-type {device_type} --badge-type IotEdgeCompatible --connection-string {connection_string} + - name: Creating test with symmetric key attestation and specified validation type + text: > + az iot product test create --attestation-type SymmetricKey --device-type {device_type} --validation-type Certification --product-id {product_id} + """ + helps[ + "iot product test search" + ] = """ + type: command + short-summary: Search product repository for testing data + examples: + - name: Search by product id + text: > + az iot product test search --product-id {product_id} + - name: Search by DPS registration + text: > + az iot product test search --registration-id {registration_id} + - name: Search by x509 certifcate common name (CN) + text: > + az iot product test search --certificate-name {certificate_name} + - name: Search by multiple values + text: > + az iot product test search --product-id {product_id} --certificate-name {certificate_name} + """ + helps[ + "iot product test show" + ] = """ + type: command + short-summary: View product test data + examples: + - name: Basic usage + text: > + az iot product test show --test-id {test_id} + """ + helps[ + "iot product test update" + ] = """ + type: command + short-summary: Update the product certification test data + examples: + - name: Basic usage + text: > + az iot product test update --test-id {test_id} --configuration-file {configuration_file} + """ + # Product Test Tasks + helps[ + "iot product test task" + ] = """ + type: group + short-summary: Manage product testing certification tasks + """ + helps[ + "iot product test task create" + ] = """ + type: command + short-summary: Queue a new testing task. Only one testing task can be running at a time + examples: + - name: Basic usage + text: > + az iot product test task create --test-id {test_id} + - name: Wait for completion and return test case + text: > + az iot product test task create --test-id {test_id} --wait + - name: Wait for completion with custom polling interval to completion and return test case + text: > + az iot product test task create --test-id {test_id} --wait --poll-interval 5 + """ + helps[ + "iot product test task delete" + ] = """ + type: command + short-summary: Cancel a running task matching the specified --task-id + examples: + - name: Basic usage + text: > + az iot product test task delete --test-id {test_id} --task-id {task_id} + """ + helps[ + "iot product test task show" + ] = """ + type: command + short-summary: Show the status of a testing task. Use --running for current running task or --task-id + examples: + - name: Task status by --task-id + text: > + az iot product test task show --test-id {test_id} --task-id {task_id} + - name: Currently running task of product test + text: > + az iot product test task show --test-id {test_id} --running + """ + # Test Cases + helps[ + "iot product test case" + ] = """ + type: group + short-summary: Manage product testing certification test cases + """ + helps[ + "iot product test case update" + ] = """ + type: command + short-summary: Update the product certification test case data + examples: + - name: Basic usage + text: > + az iot product test case update --test-id {test_id} --configuration-file {configuration_file} + """ + helps[ + "iot product test case list" + ] = """ + type: command + short-summary: List the test cases of a product certification test + examples: + - name: Basic usage + text: > + az iot product test case list --test-id {test_id} + """ + # Test Runs + helps[ + "iot product test run" + ] = """ + type: group + short-summary: Manage product testing certification test runs + """ + helps[ + "iot product test run submit" + ] = """ + type: command + short-summary: Submit a completed test run to the partner/product service + examples: + - name: Basic usage + text: > + az iot product test run submit --test-id {test_id} --run-id {run_id} + """ + helps[ + "iot product test run show" + ] = """ + type: command + short-summary: Show the status of a testing run. + examples: + - name: Latest product test run + text: > + az iot product test run show --test-id {test_id} + - name: Testing status by --run-id + text: > + az iot product test run show --test-id {test_id} --run-id {run_id} + """ diff --git a/azext_iot/product/test/command_map.py b/azext_iot/product/test/command_map.py new file mode 100644 index 000000000..7dcb0557e --- /dev/null +++ b/azext_iot/product/test/command_map.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +""" +Load CLI commands +""" +from azure.cli.core.commands import CliCommandType + +tests_ops = CliCommandType(operations_tmpl="azext_iot.product.test.command_tests#{}") + +test_tasks_ops = CliCommandType( + operations_tmpl="azext_iot.product.test.command_test_tasks#{}" +) + +test_cases_ops = CliCommandType( + operations_tmpl="azext_iot.product.test.command_test_cases#{}" +) + +test_runs_ops = CliCommandType(operations_tmpl="azext_iot.product.test.command_test_runs#{}") + + +def load_product_test_commands(self, _): + with self.command_group("iot product test", command_type=tests_ops) as g: + g.command("create", "create") + g.command("update", "update") + g.command("show", "show") + g.command("search", "search") + with self.command_group("iot product test case", command_type=test_cases_ops) as g: + g.command("list", "list") + g.command("update", "update") + with self.command_group("iot product test task", command_type=test_tasks_ops) as g: + g.command("create", "create") + g.command("delete", "delete") + g.command("show", "show") + with self.command_group("iot product test run", command_type=test_runs_ops) as g: + g.command("show", "show") + g.command("submit", "submit") diff --git a/azext_iot/product/test/command_test_cases.py b/azext_iot/product/test/command_test_cases.py new file mode 100644 index 000000000..d08d4b78c --- /dev/null +++ b/azext_iot/product/test/command_test_cases.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.product.providers.aics import AICSProvider +from azext_iot.common.utility import process_json_arg +from knack.util import CLIError + + +def list(cmd, test_id, base_url=None): + ap = AICSProvider(cmd, base_url) + return ap.show_test_cases(test_id=test_id) + + +def update(cmd, test_id, configuration_file, base_url=None): + import os + + if not os.path.exists(configuration_file): + raise CLIError("Specified configuration file does not exist") + ap = AICSProvider(cmd, base_url) + return ap.update_test_cases( + test_id=test_id, + patch=process_json_arg(configuration_file, "configuration_file"), + ) diff --git a/azext_iot/product/test/command_test_runs.py b/azext_iot/product/test/command_test_runs.py new file mode 100644 index 000000000..5537cb5cc --- /dev/null +++ b/azext_iot/product/test/command_test_runs.py @@ -0,0 +1,41 @@ +# 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 time import sleep +from azext_iot.product.providers.aics import AICSProvider +from azext_iot.product.shared import DeviceTestTaskStatus as Status +from knack.util import CLIError + + +def show(cmd, test_id, run_id=None, wait=False, poll_interval=3, base_url=None): + final_statuses = [ + Status.failed.value, + Status.completed.value, + Status.cancelled.value, + ] + ap = AICSProvider(cmd, base_url) + if run_id: + response = ap.show_test_run(test_id=test_id, run_id=run_id) + else: + response = ap.show_test_run_latest(test_id=test_id) + + if not response: + error = "No test run found for test ID '{}'".format(test_id) + if run_id: + error = error + " with run ID '{}'".format(run_id) + raise CLIError(error) + status = response.status + run_id = response.id + while all([wait, status, run_id]) and status not in final_statuses: + sleep(poll_interval) + response = ap.show_test_run(test_id=test_id, run_id=run_id) + status = response.status + return response + + +def submit(cmd, test_id, run_id, base_url=None): + ap = AICSProvider(cmd, base_url) + return ap.submit_test_run(test_id=test_id, run_id=run_id) diff --git a/azext_iot/product/test/command_test_tasks.py b/azext_iot/product/test/command_test_tasks.py new file mode 100644 index 000000000..0d997270a --- /dev/null +++ b/azext_iot/product/test/command_test_tasks.py @@ -0,0 +1,69 @@ +# 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 time import sleep +from azext_iot.product.shared import TaskType, DeviceTestTaskStatus as Status +from azext_iot.product.providers.aics import AICSProvider +from knack.util import CLIError + + +def create( + cmd, test_id, task_type=TaskType.QueueTestRun.value, wait=False, poll_interval=3, base_url=None +): + ap = AICSProvider(cmd, base_url) + final_statuses = [ + Status.failed.value, + Status.completed.value, + Status.cancelled.value, + ] + response = ap.create_test_task( + test_id=test_id, task_type=task_type, wait=wait, poll_interval=poll_interval + ) + if not response: + raise CLIError( + "Failed to create device test task - please ensure a device test exists with Id {}".format( + test_id + ) + ) + if isinstance(response, dict): + raise CLIError(response) + + status = response.status + task_id = response.id + while all([wait, status, task_id]) and status not in final_statuses: + sleep(poll_interval) + response = ap.show_test_task(test_id=test_id, task_id=task_id) + status = response.status + + # if a task of 'queueTestRun' is awaited, return the run result + if all( + [ + wait, + status in final_statuses, + task_type == TaskType.QueueTestRun.value, + response.result_link, + ] + ): + run_id = response.result_link.split("/")[-1] + return ap.show_test_run(test_id=test_id, run_id=run_id) if run_id else response + + return response + + +def delete(cmd, test_id, task_id, base_url=None): + ap = AICSProvider(cmd, base_url) + return ap.delete_test_task(test_id=test_id, task_id=task_id) + + +def show(cmd, test_id, task_id=None, running=False, base_url=None): + ap = AICSProvider(cmd, base_url) + if task_id: + return ap.show_test_task(test_id=test_id, task_id=task_id) + elif running: + return ap.show_running_test_task(test_id=test_id) + raise CLIError( + "Please provide a task-id for individual task details, or use the --running argument to list all running tasks" + ) diff --git a/azext_iot/product/test/command_tests.py b/azext_iot/product/test/command_tests.py new file mode 100644 index 000000000..d371746eb --- /dev/null +++ b/azext_iot/product/test/command_tests.py @@ -0,0 +1,296 @@ +# 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.product.providers.aics import AICSProvider +from azext_iot.sdk.product.models import DeviceTestSearchOptions +from azext_iot.product.shared import BadgeType, AttestationType, ValidationType +from knack.log import get_logger +from knack.util import CLIError +import os + +logger = get_logger(__name__) + + +def create( + cmd, + configuration_file=None, + product_id=None, + device_type=None, + attestation_type=None, + certificate_path=None, + connection_string=None, + endorsement_key=None, + badge_type=BadgeType.IotDevice.value, + validation_type=ValidationType.test.value, + models=None, + skip_provisioning=False, + base_url=None, +): + if attestation_type == AttestationType.x509.value and not certificate_path: + raise CLIError("If attestation type is x509, certificate path is required") + if attestation_type == AttestationType.tpm.value and not endorsement_key: + raise CLIError("If attestation type is TPM, endorsement key is required") + if badge_type == BadgeType.Pnp.value and not models: + raise CLIError("If badge type is Pnp, models is required") + if badge_type == BadgeType.IotEdgeCompatible.value and not all( + [connection_string, attestation_type == AttestationType.connectionString.value] + ): + raise CLIError( + "Connection string is required for Edge Compatible modules testing" + ) + if badge_type != BadgeType.IotEdgeCompatible.value and ( + connection_string or attestation_type == AttestationType.connectionString.value + ): + raise CLIError( + "Connection string is only available for Edge Compatible modules testing" + ) + if validation_type != ValidationType.test.value and not product_id: + raise CLIError( + "Product Id is required for validation type {}".format(validation_type) + ) + if not any( + [ + configuration_file, + all([device_type, attestation_type, badge_type]), + ] + ): + raise CLIError( + "If configuration file is not specified, attestation and device definition parameters must be specified" + ) + test_configuration = ( + _create_from_file(configuration_file) + if configuration_file + else _build_test_configuration( + product_id=product_id, + device_type=device_type, + attestation_type=attestation_type, + certificate_path=certificate_path, + endorsement_key=endorsement_key, + badge_type=badge_type, + connection_string=connection_string, + models=models, + validation_type=validation_type + ) + ) + + ap = AICSProvider(cmd, base_url) + + provisioning = not skip_provisioning + test_data = ap.create_test( + test_configuration=test_configuration, provisioning=provisioning + ) + + return test_data + + +def show(cmd, test_id, base_url=None): + ap = AICSProvider(cmd, base_url) + return ap.show_test(test_id) + + +def update( + cmd, + test_id, + configuration_file=None, + attestation_type=None, + certificate_path=None, + connection_string=None, + endorsement_key=None, + badge_type=None, + models=None, + base_url=None, +): + provisioning = False + # verify required parameters for various options + if attestation_type == AttestationType.x509.value and not certificate_path: + raise CLIError("If attestation type is x509, certificate path is required") + if attestation_type == AttestationType.tpm.value and not endorsement_key: + raise CLIError("If attestation type is tpm, endorsement key is required") + if badge_type == BadgeType.Pnp.value and not models: + raise CLIError("If badge type is Pnp, models is required") + if badge_type == BadgeType.IotEdgeCompatible.value and not all( + [connection_string, attestation_type == AttestationType.connectionString.value] + ): + raise CLIError( + "Connection string is required for Edge Compatible modules testing" + ) + if badge_type != BadgeType.IotEdgeCompatible.value and ( + connection_string or attestation_type == AttestationType.connectionString.value + ): + raise CLIError( + "Connection string is only available for Edge Compatible modules testing" + ) + ap = AICSProvider(cmd, base_url) + if configuration_file: + test_configuration = _create_from_file(configuration_file) + return ap.update_test( + test_id=test_id, + test_configuration=test_configuration, + provisioning=provisioning, + ) + + if not any([attestation_type, badge_type, models]): + raise CLIError( + "Configuration file, attestation information, or device configuration must be specified" + ) + + test_configuration = ap.show_test(test_id=test_id) + + provisioning_configuration = test_configuration["provisioningConfiguration"] + registration_id = provisioning_configuration["dpsRegistrationId"] + + # change attestation + if attestation_type: + # reset the provisioningConfiguration + test_configuration["provisioningConfiguration"] = { + "type": attestation_type, + "dpsRegistrationId": registration_id, + } + provisioning = True + if attestation_type == AttestationType.symmetricKey.value: + test_configuration["provisioningConfiguration"][ + "symmetricKeyEnrollmentInformation" + ] = {} + elif attestation_type == AttestationType.tpm.value: + test_configuration["provisioningConfiguration"][ + "tpmEnrollmentInformation" + ] = {"endorsementKey": endorsement_key} + elif attestation_type == AttestationType.x509.value: + test_configuration["provisioningConfiguration"][ + "x509EnrollmentInformation" + ] = { + "base64EncodedX509Certificate": _read_certificate_from_file( + certificate_path + ) + } + elif attestation_type == AttestationType.connectionString.value: + test_configuration["provisioningConfiguration"][ + "deviceConnectionString" + ] = connection_string + + # reset PnP models + badge_config = test_configuration["certificationBadgeConfigurations"] + + if ( + badge_type == BadgeType.Pnp.value + or badge_config[0]["type"].lower() == BadgeType.Pnp.value.lower() + ) and models: + models_array = _process_models_directory(models) + test_configuration["certificationBadgeConfigurations"] = [ + {"type": BadgeType.Pnp.value, "digitalTwinModelDefinitions": models_array} + ] + elif badge_type: + test_configuration["certificationBadgeConfigurations"] = [{"type": badge_type}] + + return ap.update_test( + test_id=test_id, + test_configuration=test_configuration, + provisioning=provisioning, + ) + + +def search( + cmd, product_id=None, registration_id=None, certificate_name=None, base_url=None +): + if not any([product_id or registration_id or certificate_name]): + raise CLIError("At least one search criteria must be specified") + + ap = AICSProvider(cmd, base_url) + searchOptions = DeviceTestSearchOptions( + product_id=product_id, + dps_registration_id=registration_id, + dps_x509_certificate_common_name=certificate_name, + ) + return ap.search_test(searchOptions) + + +def _build_test_configuration( + product_id, + device_type, + attestation_type, + certificate_path, + endorsement_key, + connection_string, + badge_type, + models, + validation_type +): + config = { + "validationType": validation_type, + "productId": product_id, + "deviceType": device_type, + "provisioningConfiguration": {"type": attestation_type}, + "certificationBadgeConfigurations": [{"type": badge_type}], + } + if attestation_type == AttestationType.symmetricKey.value: + config["provisioningConfiguration"]["symmetricKeyEnrollmentInformation"] = {} + elif attestation_type == AttestationType.tpm.value: + config["provisioningConfiguration"]["tpmEnrollmentInformation"] = { + "endorsementKey": endorsement_key + } + elif attestation_type == AttestationType.x509.value: + config["provisioningConfiguration"]["x509EnrollmentInformation"] = { + "base64EncodedX509Certificate": _read_certificate_from_file( + certificate_path + ) + } + elif attestation_type == AttestationType.connectionString.value: + config["provisioningConfiguration"][ + "deviceConnectionString" + ] = connection_string + + if badge_type == BadgeType.Pnp.value and models: + models_array = _process_models_directory(models) + config["certificationBadgeConfigurations"][0][ + "digitalTwinModelDefinitions" + ] = models_array + + return config + + +def _process_models_directory(from_directory): + from azext_iot.common.utility import scantree, process_json_arg, read_file_content + # we need to double-encode the JSON string + from json import dumps + + models = [] + if os.path.isfile(from_directory) and (from_directory.endswith(".json") or from_directory.endswith(".dtdl")): + models.append(dumps(read_file_content(file_path=from_directory))) + return models + for entry in scantree(from_directory): + if not any([entry.name.endswith(".json"), entry.name.endswith(".dtdl")]): + logger.debug( + "Skipping {} - model file must end with .json or .dtdl".format( + entry.path + ) + ) + continue + entry_json = process_json_arg(content=entry.path, argument_name=entry.name) + + models.append(dumps(entry_json)) + return models + + +def _read_certificate_from_file(certificate_path): + with open(file=certificate_path, mode="rb") as f: + data = f.read() + + from base64 import encodestring + + return encodestring(data) + + +def _create_from_file(configuration_file): + if not (os.path.exists(configuration_file)): + raise CLIError("Specified configuration file does not exist") + + # read the json file and POST /deviceTests + with open(file=configuration_file, encoding="utf-8") as f: + file_contents = f.read() + + from json import loads + + return loads(file_contents) diff --git a/azext_iot/product/test/params.py b/azext_iot/product/test/params.py new file mode 100644 index 000000000..0a0c40cad --- /dev/null +++ b/azext_iot/product/test/params.py @@ -0,0 +1,165 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +""" +CLI parameter definitions. +""" + +from azure.cli.core.commands.parameters import get_three_state_flag, get_enum_type +from azext_iot.product.shared import AttestationType, DeviceType, TaskType, ValidationType + + +def load_product_test_params(self, _): + with self.argument_context("iot product test") as c: + c.argument( + "skip_provisioning", + options_list=["--skip-provisioning", "--sp"], + help="Determines whether the service skips generating provisioning configuration. " + + "Only applies to SymmetricKey and ConnectionString provisioning types", + arg_group="IoT Device Certification", + ) + c.argument( + "configuration_file", + options_list=["--configuration-file", "--cf"], + help="Path to device test JSON file. " + "If not specified, attestation and device definition parameters must be specified", + arg_group="IoT Device Certification", + ) + c.argument( + "attestation_type", + options_list=["--attestation-type", "--at"], + help="How the device will authenticate to testing service Device Provisioning Service", + arg_group="IoT Device Certification Attestation", + arg_type=get_enum_type(AttestationType), + ) + c.argument( + "certificate_path", + options_list=["--certificate-path", "--cp"], + help="The path to the file containing the primary certificate. " + "When choosing x509 as attestation type, one of the certificate path is required", + arg_group="IoT Device Certification Attestation", + ) + c.argument( + "endorsement_key", + options_list=["--endorsement-key", "--ek"], + help="TPM endorsement key for a TPM device. " + "When choosing TPM as attestation type, endorsement key is required", + arg_group="IoT Device Certification Attestation", + ) + c.argument( + "connection_string", + options_list=["--connection-string", "--cs"], + help="Edge module connection string" + "When choosing IotEdgeCompatible badge type, connection string and attestaion-type of connection string are required", + arg_group="IoT Device Certification Attestation", + ) + c.argument( + "models", + options_list=["--models", "-m"], + help="Path containing Azure IoT Plug and Play interfaces implemented by the device being tested. " + "When badge type is Pnp, models is required", + arg_group="IoT Device Certification Device Definition", + ) + c.argument( + "device_type", + options_list=["--device-type", "--dt"], + help="Defines the type of device to be tested", + arg_group="IoT Device Certification Device Definition", + arg_type=get_enum_type(DeviceType), + ) + c.argument( + "product_id", + options_list=["--product-id", "-p"], + help="The submitted product id. Required when validation-type is 'Certification'.", + arg_group="IoT Device Certification Device Definition", + ) + c.argument( + "validation_type", + options_list=["--validation-type", "--vt"], + help="The type of validations testing to be performed", + arg_group="IoT Device Certification Device Definition", + arg_type=get_enum_type(ValidationType) + ) + with self.argument_context("iot product test search") as c: + c.argument( + "product_id", + options_list=["--product-id", "-p"], + help="The submitted product id", + arg_group="IoT Device Certification", + ) + c.argument( + "registration_id", + options_list=["--registration-id", "-r"], + help="The regstration Id for Device Provisioning Service", + arg_group="IoT Device Certification", + ) + c.argument( + "certificate_name", + options_list=["--certificate-name", "--cn"], + help="The x509 Certificate Common Name (cn) used for Device Provisioning Service attestation", + arg_group="IoT Device Certification", + ) + with self.argument_context("iot product test case") as c: + c.argument( + "configuration_file", + options_list=["--configuration-file", "--cf"], + help="The file path for test case configuration JSON", + arg_group="IoT Device Certification", + ) + with self.argument_context("iot product test task") as c: + c.argument( + "task_id", + options_list=["--task-id"], + help="The generated Id of the testing task", + arg_group="IoT Device Certification", + ) + c.argument( + "running", + options_list=["--running"], + help="Get the running tasks of a device test", + arg_group="IoT Device Certification", + arg_type=get_three_state_flag(), + ) + c.argument( + "task_type", + options_list=["--type"], + help="The type of task for the device test", + arg_group="IoT Device Certification", + arg_type=get_enum_type(TaskType), + ) + c.argument( + "wait", + options_list=["--wait", "-w"], + help="Wait for task completion and return test case data when available", + arg_group="IoT Device Certification", + arg_type=get_three_state_flag(), + ) + c.argument( + "poll_interval", + options_list=["--poll-interval", "--interval"], + help="Used in conjunction with --wait. Sepcifies how frequently (in seconds) polling occurs", + arg_group="IoT Device Certification", + ) + with self.argument_context("iot product test run") as c: + c.argument( + "run_id", + options_list=["--run-id", "-r"], + help="The generated Id of a test run", + arg_group="IoT Device Certification", + ) + c.argument( + "wait", + options_list=["--wait", "-w"], + help='Wait until test run status is "started" or "running"', + arg_group="IoT Device Certification", + arg_type=get_three_state_flag(), + ) + c.argument( + "poll_interval", + options_list=["--poll-interval", "--interval"], + help="Used in conjunction with --wait. Specifies how frequently (in seconds) polling occurs", + arg_group="IoT Device Certification", + ) diff --git a/azext_iot/sdk/product/__init__.py b/azext_iot/sdk/product/__init__.py new file mode 100644 index 000000000..8c6b25599 --- /dev/null +++ b/azext_iot/sdk/product/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .aicsapi import AICSAPI +from .version import VERSION + +__all__ = ['AICSAPI'] + +__version__ = VERSION + diff --git a/azext_iot/sdk/product/aicsapi.py b/azext_iot/sdk/product/aicsapi.py new file mode 100644 index 000000000..6533b8498 --- /dev/null +++ b/azext_iot/sdk/product/aicsapi.py @@ -0,0 +1,1032 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.service_client import SDKClient +from msrest import Serializer, Deserializer +from msrestazure import AzureConfiguration +from .version import VERSION +from msrest.pipeline import ClientRawResponse +from msrestazure.azure_exceptions import CloudError +import uuid +from . import models + + +class AICSAPIConfiguration(AzureConfiguration): + """Configuration for AICSAPI + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if not base_url: + base_url = 'http://localhost' + + super(AICSAPIConfiguration, self).__init__(base_url) + + self.credentials = credentials + + +class AICSAPI(SDKClient): + """AICSAPI + + :ivar config: Configuration for client. + :vartype config: AICSAPIConfiguration + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + self.config = AICSAPIConfiguration(credentials, base_url) + super(AICSAPI, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2020-05-01-preview' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + + def get_device_certification_requirements( + self, badge_type=None, custom_headers=None, raw=False, **operation_config): + """Get certification requirements. + + :param badge_type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type badge_type: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_device_certification_requirements.metadata['url'] + + # Construct parameters + query_parameters = {} + if badge_type is not None: + query_parameters['badgeType'] = self._serialize.query("badge_type", badge_type, 'str') + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('[DeviceCertificationRequirement]', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_device_certification_requirements.metadata = {'url': '/certificationRequirements'} + + def create_device_test( + self, generate_provisioning_configuration=None, body=None, custom_headers=None, raw=False, **operation_config): + """Create a new Microsoft.Azure.IoT.TestKit.Models.DeviceTest. + + :param generate_provisioning_configuration: Whether to generate + ProvisioningConfiguration info from the server, + it only applies to + Microsoft.Azure.IoT.TestKit.Shared.Models.Provisioning.ProvisioningType.SymmetricKey + and + Microsoft.Azure.IoT.TestKit.Shared.Models.Provisioning.ProvisioningType.ConnectionString + provisioning type. + :type generate_provisioning_configuration: bool + :param body: + :type body: ~product.models.DeviceTest + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.create_device_test.metadata['url'] + + # Construct parameters + query_parameters = {} + if generate_provisioning_configuration is not None: + query_parameters['GenerateProvisioningConfiguration'] = self._serialize.query("generate_provisioning_configuration", generate_provisioning_configuration, 'bool') + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if body is not None: + body_content = self._serialize.body(body, 'DeviceTest') + else: + body_content = None + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('DeviceTest', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create_device_test.metadata = {'url': '/deviceTests'} + + def get_device_test( + self, device_test_id, custom_headers=None, raw=False, **operation_config): + """Get a DeviceTest by Id. + + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_device_test.metadata['url'] + path_format_arguments = { + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('DeviceTest', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_device_test.metadata = {'url': '/deviceTests/{deviceTestId}'} + + def update_device_test( + self, device_test_id, generate_provisioning_configuration=None, body=None, custom_headers=None, raw=False, **operation_config): + """Update the DeviceTest with certain Id. + + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param generate_provisioning_configuration: Whether to generate + ProvisioningConfiguration info from the server, + it only applies to + Microsoft.Azure.IoT.TestKit.Shared.Models.Provisioning.ProvisioningType.SymmetricKey + and + Microsoft.Azure.IoT.TestKit.Shared.Models.Provisioning.ProvisioningType.ConnectionString + provisioning type. + :type generate_provisioning_configuration: bool + :param body: + :type body: ~product.models.DeviceTest + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.update_device_test.metadata['url'] + path_format_arguments = { + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + if generate_provisioning_configuration is not None: + query_parameters['GenerateProvisioningConfiguration'] = self._serialize.query("generate_provisioning_configuration", generate_provisioning_configuration, 'bool') + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if body is not None: + body_content = self._serialize.body(body, 'DeviceTest') + else: + body_content = None + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('DeviceTest', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + update_device_test.metadata = {'url': '/deviceTests/{deviceTestId}'} + + def search_device_test( + self, body=None, custom_headers=None, raw=False, **operation_config): + """Search DeviceTest. + + :param body: + :type body: ~product.models.DeviceTestSearchOptions + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.search_device_test.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if body is not None: + body_content = self._serialize.body(body, 'DeviceTestSearchOptions') + else: + body_content = None + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('[DeviceTestSearchResult]', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + search_device_test.metadata = {'url': '/deviceTests/search'} + + def create_device_test_task( + self, device_test_id, task_type=None, custom_headers=None, raw=False, **operation_config): + """Queue a new async + Microsoft.Azure.IoT.TestKit.Shared.Models.DeviceTestTaskType for a + DeviceTest. + The user can only have one running task for a DeviceTest. + + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param task_type: Possible values include: 'QueueTestRun', + 'GenerateTestCases' + :type task_type: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + body = None + if task_type is not None: + body = models.NewTaskPayload(task_type=task_type) + + api_version = "2020-05-01-preview" + + # Construct URL + url = self.create_device_test_task.metadata['url'] + path_format_arguments = { + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if body is not None: + body_content = self._serialize.body(body, 'NewTaskPayload') + else: + body_content = None + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [202, 400, 404, 409]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 202: + deserialized = self._deserialize('DeviceTestTask', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + if response.status_code == 404: + deserialized = self._deserialize('object', response) + if response.status_code == 409: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create_device_test_task.metadata = {'url': '/deviceTests/{deviceTestId}/tasks'} + + def cancel_device_test_task( + self, task_id, device_test_id, custom_headers=None, raw=False, **operation_config): + """Cancel the running tasks of a DeviceTest. + + :param task_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTestTask to retrieve. + :type task_id: str + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.cancel_device_test_task.metadata['url'] + path_format_arguments = { + 'taskId': self._serialize.url("task_id", task_id, 'str'), + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [202, 400, 404]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 400: + deserialized = self._deserialize('object', response) + if response.status_code == 404: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + cancel_device_test_task.metadata = {'url': '/deviceTests/{deviceTestId}/tasks/{taskId}'} + + def get_device_test_task( + self, task_id, device_test_id, custom_headers=None, raw=False, **operation_config): + """Get the task status of a DeviceTest. + + :param task_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTestTask to retrieve. + :type task_id: str + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_device_test_task.metadata['url'] + path_format_arguments = { + 'taskId': self._serialize.url("task_id", task_id, 'str'), + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400, 404]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('DeviceTestTask', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + if response.status_code == 404: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_device_test_task.metadata = {'url': '/deviceTests/{deviceTestId}/tasks/{taskId}'} + + def get_running_device_test_tasks( + self, device_test_id, custom_headers=None, raw=False, **operation_config): + """Get the running tasks of a DeviceTest. Current implementation only + allows one running task. + + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_running_device_test_tasks.metadata['url'] + path_format_arguments = { + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400, 404]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('[DeviceTestTask]', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + if response.status_code == 404: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_running_device_test_tasks.metadata = {'url': '/deviceTests/{deviceTestId}/tasks/running'} + + def get_test_cases( + self, device_test_id, custom_headers=None, raw=False, **operation_config): + """Get the testcases of a DeviceTest. They are generated through + Microsoft.Azure.IoT.TestKit.Shared.Models.DeviceTestTaskType.GenerateTestCases + task. + + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_test_cases.metadata['url'] + path_format_arguments = { + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400, 404]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('TestCases', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + if response.status_code == 404: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_test_cases.metadata = {'url': '/deviceTests/{deviceTestId}/TestCases'} + + def update_test_cases( + self, device_test_id, certification_badge_test_cases=None, custom_headers=None, raw=False, **operation_config): + """Update the testcases settings of a DeviceTest. The test cases cannot be + added or removed through this API. + + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param certification_badge_test_cases: + :type certification_badge_test_cases: list[object] + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + body = None + if certification_badge_test_cases is not None: + body = models.TestCases(certification_badge_test_cases=certification_badge_test_cases) + + api_version = "2020-05-01-preview" + + # Construct URL + url = self.update_test_cases.metadata['url'] + path_format_arguments = { + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + if body is not None: + body_content = self._serialize.body(body, 'TestCases') + else: + body_content = None + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204, 400, 404]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 400: + deserialized = self._deserialize('object', response) + if response.status_code == 404: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + update_test_cases.metadata = {'url': '/deviceTests/{deviceTestId}/TestCases'} + + def get_latest_test_run( + self, device_test_id, custom_headers=None, raw=False, **operation_config): + """Get the latest test run of the DeviceTest with the deviceTestId. + + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_latest_test_run.metadata['url'] + path_format_arguments = { + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400, 404]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('TestRun', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + if response.status_code == 404: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_latest_test_run.metadata = {'url': '/deviceTests/{deviceTestId}/testRuns/latest'} + + def get_test_run( + self, test_run_id, device_test_id, custom_headers=None, raw=False, **operation_config): + """Get the test run with testrunId of the DeviceTest with the + deviceTestId. + + :param test_run_id: The Id of a + Microsoft.Azure.IoT.TestKit.Models.TestRun. + :type test_run_id: str + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.get_test_run.metadata['url'] + path_format_arguments = { + 'testRunId': self._serialize.url("test_run_id", test_run_id, 'str'), + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400, 404]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('TestRun', response) + if response.status_code == 400: + deserialized = self._deserialize('object', response) + if response.status_code == 404: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_test_run.metadata = {'url': '/deviceTests/{deviceTestId}/testRuns/{testRunId}'} + + def submit_test_run( + self, test_run_id, device_test_id, custom_headers=None, raw=False, **operation_config): + """Submit TestRun to Partner/Product service. + + :param test_run_id: The Id of a + Microsoft.Azure.IoT.TestKit.Models.TestRun. + :type test_run_id: str + :param device_test_id: The Id of the + Microsoft.Azure.IoT.TestKit.Models.DeviceTest to retrieve. + :type device_test_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + api_version = "2020-05-01-preview" + + # Construct URL + url = self.submit_test_run.metadata['url'] + path_format_arguments = { + 'testRunId': self._serialize.url("test_run_id", test_run_id, 'str'), + 'deviceTestId': self._serialize.url("device_test_id", device_test_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204, 400, 404, 500]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 400: + deserialized = self._deserialize('object', response) + if response.status_code == 404: + deserialized = self._deserialize('object', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + submit_test_run.metadata = {'url': '/deviceTests/{deviceTestId}/testRuns/{testRunId}/submit'} diff --git a/azext_iot/sdk/product/models/__init__.py b/azext_iot/sdk/product/models/__init__.py new file mode 100644 index 000000000..609b4014d --- /dev/null +++ b/azext_iot/sdk/product/models/__init__.py @@ -0,0 +1,187 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +try: + from .device_certification_provisioning_requirement_py3 import DeviceCertificationProvisioningRequirement + from .device_certification_requirement_py3 import DeviceCertificationRequirement + from .badge_info_py3 import BadgeInfo + from .device_test_search_result_py3 import DeviceTestSearchResult + from .device_test_task_py3 import DeviceTestTask + from .validation_problem_details_py3 import ValidationProblemDetails + from .x509_enrollment_py3 import X509Enrollment + from .symmetric_key_enrollment_py3 import SymmetricKeyEnrollment + from .tpm_enrollment_py3 import TpmEnrollment + from .provisioning_configuration_py3 import ProvisioningConfiguration + from .iot_device_certification_badge_configuration_py3 import IotDeviceCertificationBadgeConfiguration + from .iot_edge_compatible_certification_badge_configuration_py3 import IotEdgeCompatibleCertificationBadgeConfiguration + from .model_resolution_source_py3 import ModelResolutionSource + from .pnp_certification_badge_configuration_py3 import PnpCertificationBadgeConfiguration + from .device_test_py3 import DeviceTest + from .device_test_search_options_py3 import DeviceTestSearchOptions + from .new_task_payload_py3 import NewTaskPayload + from .cannot_retrieve_model_reposistory_sas_token_error_py3 import CannotRetrieveModelReposistorySasTokenError + from .device_test_not_exist_error_py3 import DeviceTestNotExistError + from .existing_task_running_conflict_error_py3 import ExistingTaskRunningConflictError + from .fail_to_queue_task_error_py3 import FailToQueueTaskError + from .model_resolution_failure_error_py3 import ModelResolutionFailureError + from .system_error_py3 import SystemError + from .test_cases_not_exist_error_py3 import TestCasesNotExistError + from .test_run_not_exist_error_py3 import TestRunNotExistError + from .c2_dtest_py3 import C2DTest + from .d2_ctest_py3 import D2CTest + from .device_twin_test_py3 import DeviceTwinTest + from .direct_method_test_py3 import DirectMethodTest + from .iot_device_certification_badge_test_cases_py3 import IotDeviceCertificationBadgeTestCases + from .iot_edge_compatible_certification_badge_test_cases_py3 import IotEdgeCompatibleCertificationBadgeTestCases + from .interface_definition_snapshot_py3 import InterfaceDefinitionSnapshot + from .model_definition_snapshot_py3 import ModelDefinitionSnapshot + from .array_schema_py3 import ArraySchema + from .enum_value_py3 import EnumValue + from .enum_schema_py3 import EnumSchema + from .schema_field_py3 import SchemaField + from .map_schema_py3 import MapSchema + from .object_schema_py3 import ObjectSchema + from .property_model_py3 import PropertyModel + from .property_test_py3 import PropertyTest + from .command_model_py3 import CommandModel + from .command_test_py3 import CommandTest + from .telemetry_model_py3 import TelemetryModel + from .telemetry_test_py3 import TelemetryTest + from .interface_test_py3 import InterfaceTest + from .pnp_certification_badge_test_cases_py3 import PnpCertificationBadgeTestCases + from .test_cases_py3 import TestCases + from .certification_task_log_py3 import CertificationTaskLog + from .iot_device_validation_task_result_py3 import IotDeviceValidationTaskResult + from .iot_device_certification_badge_result_py3 import IotDeviceCertificationBadgeResult + from .edge_device_validation_task_result_py3 import EdgeDeviceValidationTaskResult + from .iot_edge_compatible_certification_badge_result_py3 import IotEdgeCompatibleCertificationBadgeResult + from .digital_twin_validation_task_result_py3 import DigitalTwinValidationTaskResult + from .pre_validation_task_result_py3 import PreValidationTaskResult + from .pnp_certification_badge_result_py3 import PnpCertificationBadgeResult + from .test_run_py3 import TestRun +except (SyntaxError, ImportError): + from .device_certification_provisioning_requirement import DeviceCertificationProvisioningRequirement + from .device_certification_requirement import DeviceCertificationRequirement + from .badge_info import BadgeInfo + from .device_test_search_result import DeviceTestSearchResult + from .device_test_task import DeviceTestTask + from .validation_problem_details import ValidationProblemDetails + from .x509_enrollment import X509Enrollment + from .symmetric_key_enrollment import SymmetricKeyEnrollment + from .tpm_enrollment import TpmEnrollment + from .provisioning_configuration import ProvisioningConfiguration + from .iot_device_certification_badge_configuration import IotDeviceCertificationBadgeConfiguration + from .iot_edge_compatible_certification_badge_configuration import IotEdgeCompatibleCertificationBadgeConfiguration + from .model_resolution_source import ModelResolutionSource + from .pnp_certification_badge_configuration import PnpCertificationBadgeConfiguration + from .device_test import DeviceTest + from .device_test_search_options import DeviceTestSearchOptions + from .new_task_payload import NewTaskPayload + from .cannot_retrieve_model_reposistory_sas_token_error import CannotRetrieveModelReposistorySasTokenError + from .device_test_not_exist_error import DeviceTestNotExistError + from .existing_task_running_conflict_error import ExistingTaskRunningConflictError + from .fail_to_queue_task_error import FailToQueueTaskError + from .model_resolution_failure_error import ModelResolutionFailureError + from .system_error import SystemError + from .test_cases_not_exist_error import TestCasesNotExistError + from .test_run_not_exist_error import TestRunNotExistError + from .c2_dtest import C2DTest + from .d2_ctest import D2CTest + from .device_twin_test import DeviceTwinTest + from .direct_method_test import DirectMethodTest + from .iot_device_certification_badge_test_cases import IotDeviceCertificationBadgeTestCases + from .iot_edge_compatible_certification_badge_test_cases import IotEdgeCompatibleCertificationBadgeTestCases + from .interface_definition_snapshot import InterfaceDefinitionSnapshot + from .model_definition_snapshot import ModelDefinitionSnapshot + from .array_schema import ArraySchema + from .enum_value import EnumValue + from .enum_schema import EnumSchema + from .schema_field import SchemaField + from .map_schema import MapSchema + from .object_schema import ObjectSchema + from .property_model import PropertyModel + from .property_test import PropertyTest + from .command_model import CommandModel + from .command_test import CommandTest + from .telemetry_model import TelemetryModel + from .telemetry_test import TelemetryTest + from .interface_test import InterfaceTest + from .pnp_certification_badge_test_cases import PnpCertificationBadgeTestCases + from .test_cases import TestCases + from .certification_task_log import CertificationTaskLog + from .iot_device_validation_task_result import IotDeviceValidationTaskResult + from .iot_device_certification_badge_result import IotDeviceCertificationBadgeResult + from .edge_device_validation_task_result import EdgeDeviceValidationTaskResult + from .iot_edge_compatible_certification_badge_result import IotEdgeCompatibleCertificationBadgeResult + from .digital_twin_validation_task_result import DigitalTwinValidationTaskResult + from .pre_validation_task_result import PreValidationTaskResult + from .pnp_certification_badge_result import PnpCertificationBadgeResult + from .test_run import TestRun + +__all__ = [ + 'DeviceCertificationProvisioningRequirement', + 'DeviceCertificationRequirement', + 'BadgeInfo', + 'DeviceTestSearchResult', + 'DeviceTestTask', + 'ValidationProblemDetails', + 'X509Enrollment', + 'SymmetricKeyEnrollment', + 'TpmEnrollment', + 'ProvisioningConfiguration', + 'IotDeviceCertificationBadgeConfiguration', + 'IotEdgeCompatibleCertificationBadgeConfiguration', + 'ModelResolutionSource', + 'PnpCertificationBadgeConfiguration', + 'DeviceTest', + 'DeviceTestSearchOptions', + 'NewTaskPayload', + 'CannotRetrieveModelReposistorySasTokenError', + 'DeviceTestNotExistError', + 'ExistingTaskRunningConflictError', + 'FailToQueueTaskError', + 'ModelResolutionFailureError', + 'SystemError', + 'TestCasesNotExistError', + 'TestRunNotExistError', + 'C2DTest', + 'D2CTest', + 'DeviceTwinTest', + 'DirectMethodTest', + 'IotDeviceCertificationBadgeTestCases', + 'IotEdgeCompatibleCertificationBadgeTestCases', + 'InterfaceDefinitionSnapshot', + 'ModelDefinitionSnapshot', + 'ArraySchema', + 'EnumValue', + 'EnumSchema', + 'SchemaField', + 'MapSchema', + 'ObjectSchema', + 'PropertyModel', + 'PropertyTest', + 'CommandModel', + 'CommandTest', + 'TelemetryModel', + 'TelemetryTest', + 'InterfaceTest', + 'PnpCertificationBadgeTestCases', + 'TestCases', + 'CertificationTaskLog', + 'IotDeviceValidationTaskResult', + 'IotDeviceCertificationBadgeResult', + 'EdgeDeviceValidationTaskResult', + 'IotEdgeCompatibleCertificationBadgeResult', + 'DigitalTwinValidationTaskResult', + 'PreValidationTaskResult', + 'PnpCertificationBadgeResult', + 'TestRun', +] diff --git a/azext_iot/sdk/product/models/array_schema.py b/azext_iot/sdk/product/models/array_schema.py new file mode 100644 index 000000000..cefd28d64 --- /dev/null +++ b/azext_iot/sdk/product/models/array_schema.py @@ -0,0 +1,58 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ArraySchema(Model): + """ArraySchema. + + :param element_schema: + :type element_schema: object + :param schema_type: Possible values include: 'Unknown', 'Boolean', 'Date', + 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', 'String', + 'Time', 'Enum', 'Object', 'Map', 'Array' + :type schema_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'element_schema': {'key': 'elementSchema', 'type': 'object'}, + 'schema_type': {'key': 'schemaType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(ArraySchema, self).__init__(**kwargs) + self.element_schema = kwargs.get('element_schema', None) + self.schema_type = kwargs.get('schema_type', None) + self.name = kwargs.get('name', None) + self.comment = kwargs.get('comment', None) + self.display_name = kwargs.get('display_name', None) + self.id = kwargs.get('id', None) + self.description = kwargs.get('description', None) + self.language_version = kwargs.get('language_version', None) diff --git a/azext_iot/sdk/product/models/array_schema_py3.py b/azext_iot/sdk/product/models/array_schema_py3.py new file mode 100644 index 000000000..48bda58a1 --- /dev/null +++ b/azext_iot/sdk/product/models/array_schema_py3.py @@ -0,0 +1,58 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ArraySchema(Model): + """ArraySchema. + + :param element_schema: + :type element_schema: object + :param schema_type: Possible values include: 'Unknown', 'Boolean', 'Date', + 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', 'String', + 'Time', 'Enum', 'Object', 'Map', 'Array' + :type schema_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'element_schema': {'key': 'elementSchema', 'type': 'object'}, + 'schema_type': {'key': 'schemaType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, *, element_schema=None, schema_type=None, name: str=None, comment: str=None, display_name=None, id: str=None, description=None, language_version: int=None, **kwargs) -> None: + super(ArraySchema, self).__init__(**kwargs) + self.element_schema = element_schema + self.schema_type = schema_type + self.name = name + self.comment = comment + self.display_name = display_name + self.id = id + self.description = description + self.language_version = language_version diff --git a/azext_iot/sdk/product/models/badge_info.py b/azext_iot/sdk/product/models/badge_info.py new file mode 100644 index 000000000..c1ada7c11 --- /dev/null +++ b/azext_iot/sdk/product/models/badge_info.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BadgeInfo(Model): + """BadgeInfo. + + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + :param os: + :type os: str + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'os': {'key': 'os', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(BadgeInfo, self).__init__(**kwargs) + self.type = kwargs.get('type', None) + self.os = kwargs.get('os', None) diff --git a/azext_iot/sdk/product/models/badge_info_py3.py b/azext_iot/sdk/product/models/badge_info_py3.py new file mode 100644 index 000000000..a62c838ca --- /dev/null +++ b/azext_iot/sdk/product/models/badge_info_py3.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BadgeInfo(Model): + """BadgeInfo. + + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + :param os: + :type os: str + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'os': {'key': 'os', 'type': 'str'}, + } + + def __init__(self, *, type=None, os: str=None, **kwargs) -> None: + super(BadgeInfo, self).__init__(**kwargs) + self.type = type + self.os = os diff --git a/azext_iot/sdk/product/models/c2_dtest.py b/azext_iot/sdk/product/models/c2_dtest.py new file mode 100644 index 000000000..d62ffad51 --- /dev/null +++ b/azext_iot/sdk/product/models/c2_dtest.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class C2DTest(Model): + """C2DTest. + + :param message_to_send: + :type message_to_send: str + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'message_to_send': {'key': 'messageToSend', 'type': 'str'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(C2DTest, self).__init__(**kwargs) + self.message_to_send = kwargs.get('message_to_send', None) + self.validation_timeout = kwargs.get('validation_timeout', None) + self.is_mandatory = kwargs.get('is_mandatory', None) + self.should_validate = kwargs.get('should_validate', None) diff --git a/azext_iot/sdk/product/models/c2_dtest_py3.py b/azext_iot/sdk/product/models/c2_dtest_py3.py new file mode 100644 index 000000000..fcab04699 --- /dev/null +++ b/azext_iot/sdk/product/models/c2_dtest_py3.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class C2DTest(Model): + """C2DTest. + + :param message_to_send: + :type message_to_send: str + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'message_to_send': {'key': 'messageToSend', 'type': 'str'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, *, message_to_send: str=None, validation_timeout: int=None, is_mandatory: bool=None, should_validate: bool=None, **kwargs) -> None: + super(C2DTest, self).__init__(**kwargs) + self.message_to_send = message_to_send + self.validation_timeout = validation_timeout + self.is_mandatory = is_mandatory + self.should_validate = should_validate diff --git a/azext_iot/sdk/product/models/cannot_retrieve_model_reposistory_sas_token_error.py b/azext_iot/sdk/product/models/cannot_retrieve_model_reposistory_sas_token_error.py new file mode 100644 index 000000000..1ac686b6f --- /dev/null +++ b/azext_iot/sdk/product/models/cannot_retrieve_model_reposistory_sas_token_error.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CannotRetrieveModelReposistorySasTokenError(Model): + """CannotRetrieveModelReposistorySasTokenError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(CannotRetrieveModelReposistorySasTokenError, self).__init__(**kwargs) + self.message = kwargs.get('message', None) + self.code = kwargs.get('code', None) + self.details = kwargs.get('details', None) diff --git a/azext_iot/sdk/product/models/cannot_retrieve_model_reposistory_sas_token_error_py3.py b/azext_iot/sdk/product/models/cannot_retrieve_model_reposistory_sas_token_error_py3.py new file mode 100644 index 000000000..8d27e0b0f --- /dev/null +++ b/azext_iot/sdk/product/models/cannot_retrieve_model_reposistory_sas_token_error_py3.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CannotRetrieveModelReposistorySasTokenError(Model): + """CannotRetrieveModelReposistorySasTokenError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, *, message: str=None, code: int=None, details=None, **kwargs) -> None: + super(CannotRetrieveModelReposistorySasTokenError, self).__init__(**kwargs) + self.message = message + self.code = code + self.details = details diff --git a/azext_iot/sdk/product/models/certification_task_log.py b/azext_iot/sdk/product/models/certification_task_log.py new file mode 100644 index 000000000..781e65141 --- /dev/null +++ b/azext_iot/sdk/product/models/certification_task_log.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CertificationTaskLog(Model): + """CertificationTaskLog. + + :param time: + :type time: datetime + :param message: + :type message: str + """ + + _attribute_map = { + 'time': {'key': 'time', 'type': 'iso-8601'}, + 'message': {'key': 'message', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(CertificationTaskLog, self).__init__(**kwargs) + self.time = kwargs.get('time', None) + self.message = kwargs.get('message', None) diff --git a/azext_iot/sdk/product/models/certification_task_log_py3.py b/azext_iot/sdk/product/models/certification_task_log_py3.py new file mode 100644 index 000000000..0067a1d1b --- /dev/null +++ b/azext_iot/sdk/product/models/certification_task_log_py3.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CertificationTaskLog(Model): + """CertificationTaskLog. + + :param time: + :type time: datetime + :param message: + :type message: str + """ + + _attribute_map = { + 'time': {'key': 'time', 'type': 'iso-8601'}, + 'message': {'key': 'message', 'type': 'str'}, + } + + def __init__(self, *, time=None, message: str=None, **kwargs) -> None: + super(CertificationTaskLog, self).__init__(**kwargs) + self.time = time + self.message = message diff --git a/azext_iot/sdk/product/models/command_model.py b/azext_iot/sdk/product/models/command_model.py new file mode 100644 index 000000000..e1b247a91 --- /dev/null +++ b/azext_iot/sdk/product/models/command_model.py @@ -0,0 +1,61 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CommandModel(Model): + """CommandModel. + + :param request_schema: + :type request_schema: ~product.models.SchemaField + :param response_schema: + :type response_schema: ~product.models.SchemaField + :param command_execution_type: Possible values include: 'Synchronous', + 'Asynchronous' + :type command_execution_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'request_schema': {'key': 'requestSchema', 'type': 'SchemaField'}, + 'response_schema': {'key': 'responseSchema', 'type': 'SchemaField'}, + 'command_execution_type': {'key': 'commandExecutionType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(CommandModel, self).__init__(**kwargs) + self.request_schema = kwargs.get('request_schema', None) + self.response_schema = kwargs.get('response_schema', None) + self.command_execution_type = kwargs.get('command_execution_type', None) + self.name = kwargs.get('name', None) + self.comment = kwargs.get('comment', None) + self.display_name = kwargs.get('display_name', None) + self.id = kwargs.get('id', None) + self.description = kwargs.get('description', None) + self.language_version = kwargs.get('language_version', None) diff --git a/azext_iot/sdk/product/models/command_model_py3.py b/azext_iot/sdk/product/models/command_model_py3.py new file mode 100644 index 000000000..e14565990 --- /dev/null +++ b/azext_iot/sdk/product/models/command_model_py3.py @@ -0,0 +1,61 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CommandModel(Model): + """CommandModel. + + :param request_schema: + :type request_schema: ~product.models.SchemaField + :param response_schema: + :type response_schema: ~product.models.SchemaField + :param command_execution_type: Possible values include: 'Synchronous', + 'Asynchronous' + :type command_execution_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'request_schema': {'key': 'requestSchema', 'type': 'SchemaField'}, + 'response_schema': {'key': 'responseSchema', 'type': 'SchemaField'}, + 'command_execution_type': {'key': 'commandExecutionType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, *, request_schema=None, response_schema=None, command_execution_type=None, name: str=None, comment: str=None, display_name=None, id: str=None, description=None, language_version: int=None, **kwargs) -> None: + super(CommandModel, self).__init__(**kwargs) + self.request_schema = request_schema + self.response_schema = response_schema + self.command_execution_type = command_execution_type + self.name = name + self.comment = comment + self.display_name = display_name + self.id = id + self.description = description + self.language_version = language_version diff --git a/azext_iot/sdk/product/models/command_test.py b/azext_iot/sdk/product/models/command_test.py new file mode 100644 index 000000000..c27ce1b02 --- /dev/null +++ b/azext_iot/sdk/product/models/command_test.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CommandTest(Model): + """CommandTest. + + :param command: + :type command: ~product.models.CommandModel + :param payload: + :type payload: str + :param expected_result: + :type expected_result: str + :param priority: + :type priority: int + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'command': {'key': 'command', 'type': 'CommandModel'}, + 'payload': {'key': 'payload', 'type': 'str'}, + 'expected_result': {'key': 'expectedResult', 'type': 'str'}, + 'priority': {'key': 'priority', 'type': 'int'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(CommandTest, self).__init__(**kwargs) + self.command = kwargs.get('command', None) + self.payload = kwargs.get('payload', None) + self.expected_result = kwargs.get('expected_result', None) + self.priority = kwargs.get('priority', None) + self.validation_timeout = kwargs.get('validation_timeout', None) + self.is_mandatory = kwargs.get('is_mandatory', None) + self.should_validate = kwargs.get('should_validate', None) diff --git a/azext_iot/sdk/product/models/command_test_py3.py b/azext_iot/sdk/product/models/command_test_py3.py new file mode 100644 index 000000000..af077e7d7 --- /dev/null +++ b/azext_iot/sdk/product/models/command_test_py3.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CommandTest(Model): + """CommandTest. + + :param command: + :type command: ~product.models.CommandModel + :param payload: + :type payload: str + :param expected_result: + :type expected_result: str + :param priority: + :type priority: int + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'command': {'key': 'command', 'type': 'CommandModel'}, + 'payload': {'key': 'payload', 'type': 'str'}, + 'expected_result': {'key': 'expectedResult', 'type': 'str'}, + 'priority': {'key': 'priority', 'type': 'int'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, *, command=None, payload: str=None, expected_result: str=None, priority: int=None, validation_timeout: int=None, is_mandatory: bool=None, should_validate: bool=None, **kwargs) -> None: + super(CommandTest, self).__init__(**kwargs) + self.command = command + self.payload = payload + self.expected_result = expected_result + self.priority = priority + self.validation_timeout = validation_timeout + self.is_mandatory = is_mandatory + self.should_validate = should_validate diff --git a/azext_iot/sdk/product/models/d2_ctest.py b/azext_iot/sdk/product/models/d2_ctest.py new file mode 100644 index 000000000..044197111 --- /dev/null +++ b/azext_iot/sdk/product/models/d2_ctest.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class D2CTest(Model): + """D2CTest. + + :param expected_message: + :type expected_message: str + :param expected_message_count: + :type expected_message_count: int + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'expected_message': {'key': 'expectedMessage', 'type': 'str'}, + 'expected_message_count': {'key': 'expectedMessageCount', 'type': 'int'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(D2CTest, self).__init__(**kwargs) + self.expected_message = kwargs.get('expected_message', None) + self.expected_message_count = kwargs.get('expected_message_count', None) + self.validation_timeout = kwargs.get('validation_timeout', None) + self.is_mandatory = kwargs.get('is_mandatory', None) + self.should_validate = kwargs.get('should_validate', None) diff --git a/azext_iot/sdk/product/models/d2_ctest_py3.py b/azext_iot/sdk/product/models/d2_ctest_py3.py new file mode 100644 index 000000000..5cc6f9d0b --- /dev/null +++ b/azext_iot/sdk/product/models/d2_ctest_py3.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class D2CTest(Model): + """D2CTest. + + :param expected_message: + :type expected_message: str + :param expected_message_count: + :type expected_message_count: int + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'expected_message': {'key': 'expectedMessage', 'type': 'str'}, + 'expected_message_count': {'key': 'expectedMessageCount', 'type': 'int'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, *, expected_message: str=None, expected_message_count: int=None, validation_timeout: int=None, is_mandatory: bool=None, should_validate: bool=None, **kwargs) -> None: + super(D2CTest, self).__init__(**kwargs) + self.expected_message = expected_message + self.expected_message_count = expected_message_count + self.validation_timeout = validation_timeout + self.is_mandatory = is_mandatory + self.should_validate = should_validate diff --git a/azext_iot/sdk/product/models/device_certification_provisioning_requirement.py b/azext_iot/sdk/product/models/device_certification_provisioning_requirement.py new file mode 100644 index 000000000..f08571dc8 --- /dev/null +++ b/azext_iot/sdk/product/models/device_certification_provisioning_requirement.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceCertificationProvisioningRequirement(Model): + """DeviceCertificationProvisioningRequirement. + + :param provisioning_types: + :type provisioning_types: list[str] + """ + + _attribute_map = { + 'provisioning_types': {'key': 'provisioningTypes', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(DeviceCertificationProvisioningRequirement, self).__init__(**kwargs) + self.provisioning_types = kwargs.get('provisioning_types', None) diff --git a/azext_iot/sdk/product/models/device_certification_provisioning_requirement_py3.py b/azext_iot/sdk/product/models/device_certification_provisioning_requirement_py3.py new file mode 100644 index 000000000..1cc325e47 --- /dev/null +++ b/azext_iot/sdk/product/models/device_certification_provisioning_requirement_py3.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceCertificationProvisioningRequirement(Model): + """DeviceCertificationProvisioningRequirement. + + :param provisioning_types: + :type provisioning_types: list[str] + """ + + _attribute_map = { + 'provisioning_types': {'key': 'provisioningTypes', 'type': '[str]'}, + } + + def __init__(self, *, provisioning_types=None, **kwargs) -> None: + super(DeviceCertificationProvisioningRequirement, self).__init__(**kwargs) + self.provisioning_types = provisioning_types diff --git a/azext_iot/sdk/product/models/device_certification_requirement.py b/azext_iot/sdk/product/models/device_certification_requirement.py new file mode 100644 index 000000000..9dbd5b894 --- /dev/null +++ b/azext_iot/sdk/product/models/device_certification_requirement.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceCertificationRequirement(Model): + """DeviceCertificationRequirement. + + :param badge_type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type badge_type: str or ~product.models.enum + :param provisioning_requirement: + :type provisioning_requirement: + ~product.models.DeviceCertificationProvisioningRequirement + """ + + _attribute_map = { + 'badge_type': {'key': 'badgeType', 'type': 'str'}, + 'provisioning_requirement': {'key': 'provisioningRequirement', 'type': 'DeviceCertificationProvisioningRequirement'}, + } + + def __init__(self, **kwargs): + super(DeviceCertificationRequirement, self).__init__(**kwargs) + self.badge_type = kwargs.get('badge_type', None) + self.provisioning_requirement = kwargs.get('provisioning_requirement', None) diff --git a/azext_iot/sdk/product/models/device_certification_requirement_py3.py b/azext_iot/sdk/product/models/device_certification_requirement_py3.py new file mode 100644 index 000000000..a8f543813 --- /dev/null +++ b/azext_iot/sdk/product/models/device_certification_requirement_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceCertificationRequirement(Model): + """DeviceCertificationRequirement. + + :param badge_type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type badge_type: str or ~product.models.enum + :param provisioning_requirement: + :type provisioning_requirement: + ~product.models.DeviceCertificationProvisioningRequirement + """ + + _attribute_map = { + 'badge_type': {'key': 'badgeType', 'type': 'str'}, + 'provisioning_requirement': {'key': 'provisioningRequirement', 'type': 'DeviceCertificationProvisioningRequirement'}, + } + + def __init__(self, *, badge_type=None, provisioning_requirement=None, **kwargs) -> None: + super(DeviceCertificationRequirement, self).__init__(**kwargs) + self.badge_type = badge_type + self.provisioning_requirement = provisioning_requirement diff --git a/azext_iot/sdk/product/models/device_test.py b/azext_iot/sdk/product/models/device_test.py new file mode 100644 index 000000000..ea38905f3 --- /dev/null +++ b/azext_iot/sdk/product/models/device_test.py @@ -0,0 +1,59 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTest(Model): + """A DeviceTest contains basic information of a testing device and the + provisioning + information of that device in AICS's managed IoT Hubs. + + :param id: Id of a Microsoft.Azure.IoT.TestKit.Models.DeviceTest. It is + generated by the service. + :type id: str + :param validation_type: + Microsoft.Azure.IoT.TestKit.Shared.Models.ValidationType of a + Microsoft.Azure.IoT.TestKit.Models.DeviceTest. Possible values include: + 'Certification', 'Test' + :type validation_type: str or ~product.models.enum + :param product_id: Product Id of the testing device in product service. In + CLI scenario, this can be null. + :type product_id: str + :param device_type: Microsoft.Azure.IoT.TestKit.Shared.Models.DeviceType + of the testing device. Possible values include: 'FinishedProduct', + 'DevKit' + :type device_type: str or ~product.models.enum + :param provisioning_configuration: + :type provisioning_configuration: + ~product.models.ProvisioningConfiguration + :param certification_badge_configurations: Certification badge + configurations + :type certification_badge_configurations: list[object] + """ + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'validation_type': {'key': 'validationType', 'type': 'str'}, + 'product_id': {'key': 'productId', 'type': 'str'}, + 'device_type': {'key': 'deviceType', 'type': 'str'}, + 'provisioning_configuration': {'key': 'provisioningConfiguration', 'type': 'ProvisioningConfiguration'}, + 'certification_badge_configurations': {'key': 'certificationBadgeConfigurations', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(DeviceTest, self).__init__(**kwargs) + self.id = kwargs.get('id', None) + self.validation_type = kwargs.get('validation_type', None) + self.product_id = kwargs.get('product_id', None) + self.device_type = kwargs.get('device_type', None) + self.provisioning_configuration = kwargs.get('provisioning_configuration', None) + self.certification_badge_configurations = kwargs.get('certification_badge_configurations', None) diff --git a/azext_iot/sdk/product/models/device_test_not_exist_error.py b/azext_iot/sdk/product/models/device_test_not_exist_error.py new file mode 100644 index 000000000..c7672afd9 --- /dev/null +++ b/azext_iot/sdk/product/models/device_test_not_exist_error.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTestNotExistError(Model): + """DeviceTestNotExistError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(DeviceTestNotExistError, self).__init__(**kwargs) + self.message = kwargs.get('message', None) + self.code = kwargs.get('code', None) + self.details = kwargs.get('details', None) diff --git a/azext_iot/sdk/product/models/device_test_not_exist_error_py3.py b/azext_iot/sdk/product/models/device_test_not_exist_error_py3.py new file mode 100644 index 000000000..4d1be9b1a --- /dev/null +++ b/azext_iot/sdk/product/models/device_test_not_exist_error_py3.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTestNotExistError(Model): + """DeviceTestNotExistError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, *, message: str=None, code: int=None, details=None, **kwargs) -> None: + super(DeviceTestNotExistError, self).__init__(**kwargs) + self.message = message + self.code = code + self.details = details diff --git a/azext_iot/sdk/product/models/device_test_py3.py b/azext_iot/sdk/product/models/device_test_py3.py new file mode 100644 index 000000000..7ebef29ac --- /dev/null +++ b/azext_iot/sdk/product/models/device_test_py3.py @@ -0,0 +1,59 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTest(Model): + """A DeviceTest contains basic information of a testing device and the + provisioning + information of that device in AICS's managed IoT Hubs. + + :param id: Id of a Microsoft.Azure.IoT.TestKit.Models.DeviceTest. It is + generated by the service. + :type id: str + :param validation_type: + Microsoft.Azure.IoT.TestKit.Shared.Models.ValidationType of a + Microsoft.Azure.IoT.TestKit.Models.DeviceTest. Possible values include: + 'Certification', 'Test' + :type validation_type: str or ~product.models.enum + :param product_id: Product Id of the testing device in product service. In + CLI scenario, this can be null. + :type product_id: str + :param device_type: Microsoft.Azure.IoT.TestKit.Shared.Models.DeviceType + of the testing device. Possible values include: 'FinishedProduct', + 'DevKit' + :type device_type: str or ~product.models.enum + :param provisioning_configuration: + :type provisioning_configuration: + ~product.models.ProvisioningConfiguration + :param certification_badge_configurations: Certification badge + configurations + :type certification_badge_configurations: list[object] + """ + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'validation_type': {'key': 'validationType', 'type': 'str'}, + 'product_id': {'key': 'productId', 'type': 'str'}, + 'device_type': {'key': 'deviceType', 'type': 'str'}, + 'provisioning_configuration': {'key': 'provisioningConfiguration', 'type': 'ProvisioningConfiguration'}, + 'certification_badge_configurations': {'key': 'certificationBadgeConfigurations', 'type': '[object]'}, + } + + def __init__(self, *, id: str=None, validation_type=None, product_id: str=None, device_type=None, provisioning_configuration=None, certification_badge_configurations=None, **kwargs) -> None: + super(DeviceTest, self).__init__(**kwargs) + self.id = id + self.validation_type = validation_type + self.product_id = product_id + self.device_type = device_type + self.provisioning_configuration = provisioning_configuration + self.certification_badge_configurations = certification_badge_configurations diff --git a/azext_iot/sdk/product/models/device_test_search_options.py b/azext_iot/sdk/product/models/device_test_search_options.py new file mode 100644 index 000000000..510620ef6 --- /dev/null +++ b/azext_iot/sdk/product/models/device_test_search_options.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTestSearchOptions(Model): + """DeviceTestSearchOptions. + + :param product_id: + :type product_id: str + :param dps_registration_id: + :type dps_registration_id: str + :param dps_x509_certificate_common_name: + :type dps_x509_certificate_common_name: str + """ + + _attribute_map = { + 'product_id': {'key': 'productId', 'type': 'str'}, + 'dps_registration_id': {'key': 'dpsRegistrationId', 'type': 'str'}, + 'dps_x509_certificate_common_name': {'key': 'dpsX509CertificateCommonName', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DeviceTestSearchOptions, self).__init__(**kwargs) + self.product_id = kwargs.get('product_id', None) + self.dps_registration_id = kwargs.get('dps_registration_id', None) + self.dps_x509_certificate_common_name = kwargs.get('dps_x509_certificate_common_name', None) diff --git a/azext_iot/sdk/product/models/device_test_search_options_py3.py b/azext_iot/sdk/product/models/device_test_search_options_py3.py new file mode 100644 index 000000000..ffdecb882 --- /dev/null +++ b/azext_iot/sdk/product/models/device_test_search_options_py3.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTestSearchOptions(Model): + """DeviceTestSearchOptions. + + :param product_id: + :type product_id: str + :param dps_registration_id: + :type dps_registration_id: str + :param dps_x509_certificate_common_name: + :type dps_x509_certificate_common_name: str + """ + + _attribute_map = { + 'product_id': {'key': 'productId', 'type': 'str'}, + 'dps_registration_id': {'key': 'dpsRegistrationId', 'type': 'str'}, + 'dps_x509_certificate_common_name': {'key': 'dpsX509CertificateCommonName', 'type': 'str'}, + } + + def __init__(self, *, product_id: str=None, dps_registration_id: str=None, dps_x509_certificate_common_name: str=None, **kwargs) -> None: + super(DeviceTestSearchOptions, self).__init__(**kwargs) + self.product_id = product_id + self.dps_registration_id = dps_registration_id + self.dps_x509_certificate_common_name = dps_x509_certificate_common_name diff --git a/azext_iot/sdk/product/models/device_test_search_result.py b/azext_iot/sdk/product/models/device_test_search_result.py new file mode 100644 index 000000000..b85ba3f9b --- /dev/null +++ b/azext_iot/sdk/product/models/device_test_search_result.py @@ -0,0 +1,46 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTestSearchResult(Model): + """DeviceTest search result. + + :param device_test_link: Gets or sets DeviceTest resource link. + :type device_test_link: str + :param product_id: Gets or sets product Id. + :type product_id: str + :param dps_registration_id: Gets or sets DPS registration Id. + :type dps_registration_id: str + :param dps_x509_certificate_common_name: Gets or sets dPS x509 enrollment + certificate common name. + :type dps_x509_certificate_common_name: str + :param badge_info: Gets or sets list of Badge related info which can + identify a DeviceTest + :type badge_info: list[~product.models.BadgeInfo] + """ + + _attribute_map = { + 'device_test_link': {'key': 'deviceTestLink', 'type': 'str'}, + 'product_id': {'key': 'productId', 'type': 'str'}, + 'dps_registration_id': {'key': 'dpsRegistrationId', 'type': 'str'}, + 'dps_x509_certificate_common_name': {'key': 'dpsX509CertificateCommonName', 'type': 'str'}, + 'badge_info': {'key': 'badgeInfo', 'type': '[BadgeInfo]'}, + } + + def __init__(self, **kwargs): + super(DeviceTestSearchResult, self).__init__(**kwargs) + self.device_test_link = kwargs.get('device_test_link', None) + self.product_id = kwargs.get('product_id', None) + self.dps_registration_id = kwargs.get('dps_registration_id', None) + self.dps_x509_certificate_common_name = kwargs.get('dps_x509_certificate_common_name', None) + self.badge_info = kwargs.get('badge_info', None) diff --git a/azext_iot/sdk/product/models/device_test_search_result_py3.py b/azext_iot/sdk/product/models/device_test_search_result_py3.py new file mode 100644 index 000000000..cb2d3bf1b --- /dev/null +++ b/azext_iot/sdk/product/models/device_test_search_result_py3.py @@ -0,0 +1,46 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTestSearchResult(Model): + """DeviceTest search result. + + :param device_test_link: Gets or sets DeviceTest resource link. + :type device_test_link: str + :param product_id: Gets or sets product Id. + :type product_id: str + :param dps_registration_id: Gets or sets DPS registration Id. + :type dps_registration_id: str + :param dps_x509_certificate_common_name: Gets or sets dPS x509 enrollment + certificate common name. + :type dps_x509_certificate_common_name: str + :param badge_info: Gets or sets list of Badge related info which can + identify a DeviceTest + :type badge_info: list[~product.models.BadgeInfo] + """ + + _attribute_map = { + 'device_test_link': {'key': 'deviceTestLink', 'type': 'str'}, + 'product_id': {'key': 'productId', 'type': 'str'}, + 'dps_registration_id': {'key': 'dpsRegistrationId', 'type': 'str'}, + 'dps_x509_certificate_common_name': {'key': 'dpsX509CertificateCommonName', 'type': 'str'}, + 'badge_info': {'key': 'badgeInfo', 'type': '[BadgeInfo]'}, + } + + def __init__(self, *, device_test_link: str=None, product_id: str=None, dps_registration_id: str=None, dps_x509_certificate_common_name: str=None, badge_info=None, **kwargs) -> None: + super(DeviceTestSearchResult, self).__init__(**kwargs) + self.device_test_link = device_test_link + self.product_id = product_id + self.dps_registration_id = dps_registration_id + self.dps_x509_certificate_common_name = dps_x509_certificate_common_name + self.badge_info = badge_info diff --git a/azext_iot/sdk/product/models/device_test_task.py b/azext_iot/sdk/product/models/device_test_task.py new file mode 100644 index 000000000..c5a9402ea --- /dev/null +++ b/azext_iot/sdk/product/models/device_test_task.py @@ -0,0 +1,53 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTestTask(Model): + """It represents an async operation associated with a + Microsoft.Azure.IoT.TestKit.Models.DeviceTest. + + :param id: Id of a Microsoft.Azure.IoT.TestKit.Models.DeviceTestTask. + :type id: str + :param status: Task status a + Microsoft.Azure.IoT.TestKit.Models.DeviceTestTask. Possible values + include: 'Queued', 'Started', 'Running', 'Completed', 'Failed', + 'Cancelled' + :type status: str or ~product.models.enum + :param type: Task type. Possible values include: 'QueueTestRun', + 'GenerateTestCases' + :type type: str or ~product.models.enum + :param device_test_id: Id of Microsoft.Azure.IoT.TestKit.Models.DeviceTest + :type device_test_id: str + :param result_link: Task result link(relative url). + :type result_link: str + :param error: + :type error: object + """ + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'status': {'key': 'status', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'device_test_id': {'key': 'deviceTestId', 'type': 'str'}, + 'result_link': {'key': 'resultLink', 'type': 'str'}, + 'error': {'key': 'error', 'type': 'object'}, + } + + def __init__(self, **kwargs): + super(DeviceTestTask, self).__init__(**kwargs) + self.id = kwargs.get('id', None) + self.status = kwargs.get('status', None) + self.type = kwargs.get('type', None) + self.device_test_id = kwargs.get('device_test_id', None) + self.result_link = kwargs.get('result_link', None) + self.error = kwargs.get('error', None) diff --git a/azext_iot/sdk/product/models/device_test_task_py3.py b/azext_iot/sdk/product/models/device_test_task_py3.py new file mode 100644 index 000000000..2fb4bf134 --- /dev/null +++ b/azext_iot/sdk/product/models/device_test_task_py3.py @@ -0,0 +1,53 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTestTask(Model): + """It represents an async operation associated with a + Microsoft.Azure.IoT.TestKit.Models.DeviceTest. + + :param id: Id of a Microsoft.Azure.IoT.TestKit.Models.DeviceTestTask. + :type id: str + :param status: Task status a + Microsoft.Azure.IoT.TestKit.Models.DeviceTestTask. Possible values + include: 'Queued', 'Started', 'Running', 'Completed', 'Failed', + 'Cancelled' + :type status: str or ~product.models.enum + :param type: Task type. Possible values include: 'QueueTestRun', + 'GenerateTestCases' + :type type: str or ~product.models.enum + :param device_test_id: Id of Microsoft.Azure.IoT.TestKit.Models.DeviceTest + :type device_test_id: str + :param result_link: Task result link(relative url). + :type result_link: str + :param error: + :type error: object + """ + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'status': {'key': 'status', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'device_test_id': {'key': 'deviceTestId', 'type': 'str'}, + 'result_link': {'key': 'resultLink', 'type': 'str'}, + 'error': {'key': 'error', 'type': 'object'}, + } + + def __init__(self, *, id: str=None, status=None, type=None, device_test_id: str=None, result_link: str=None, error=None, **kwargs) -> None: + super(DeviceTestTask, self).__init__(**kwargs) + self.id = id + self.status = status + self.type = type + self.device_test_id = device_test_id + self.result_link = result_link + self.error = error diff --git a/azext_iot/sdk/product/models/device_twin_test.py b/azext_iot/sdk/product/models/device_twin_test.py new file mode 100644 index 000000000..ce7f47c31 --- /dev/null +++ b/azext_iot/sdk/product/models/device_twin_test.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTwinTest(Model): + """DeviceTwinTest. + + :param desired_properties: + :type desired_properties: str + :param reported_properties: + :type reported_properties: str + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'desired_properties': {'key': 'desiredProperties', 'type': 'str'}, + 'reported_properties': {'key': 'reportedProperties', 'type': 'str'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(DeviceTwinTest, self).__init__(**kwargs) + self.desired_properties = kwargs.get('desired_properties', None) + self.reported_properties = kwargs.get('reported_properties', None) + self.validation_timeout = kwargs.get('validation_timeout', None) + self.is_mandatory = kwargs.get('is_mandatory', None) + self.should_validate = kwargs.get('should_validate', None) diff --git a/azext_iot/sdk/product/models/device_twin_test_py3.py b/azext_iot/sdk/product/models/device_twin_test_py3.py new file mode 100644 index 000000000..7fece51c5 --- /dev/null +++ b/azext_iot/sdk/product/models/device_twin_test_py3.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceTwinTest(Model): + """DeviceTwinTest. + + :param desired_properties: + :type desired_properties: str + :param reported_properties: + :type reported_properties: str + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'desired_properties': {'key': 'desiredProperties', 'type': 'str'}, + 'reported_properties': {'key': 'reportedProperties', 'type': 'str'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, *, desired_properties: str=None, reported_properties: str=None, validation_timeout: int=None, is_mandatory: bool=None, should_validate: bool=None, **kwargs) -> None: + super(DeviceTwinTest, self).__init__(**kwargs) + self.desired_properties = desired_properties + self.reported_properties = reported_properties + self.validation_timeout = validation_timeout + self.is_mandatory = is_mandatory + self.should_validate = should_validate diff --git a/azext_iot/sdk/product/models/digital_twin_validation_task_result.py b/azext_iot/sdk/product/models/digital_twin_validation_task_result.py new file mode 100644 index 000000000..281c0c2cb --- /dev/null +++ b/azext_iot/sdk/product/models/digital_twin_validation_task_result.py @@ -0,0 +1,74 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinValidationTaskResult(Model): + """DigitalTwinValidationTaskResult. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param interface_id: + :type interface_id: str + :param component_name: + :type component_name: str + :param name: + :type name: str + :param timeout: + :type timeout: int + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Created', 'Running', 'Failed', + 'Passed', 'Cancelled', 'Aborted' + :type status: str or ~product.models.enum + :ivar message: + :vartype message: str + :ivar logs: + :vartype logs: list[~product.models.CertificationTaskLog] + :ivar raw_data: + :vartype raw_data: list[str] + """ + + _validation = { + 'message': {'readonly': True}, + 'logs': {'readonly': True}, + 'raw_data': {'readonly': True}, + } + + _attribute_map = { + 'interface_id': {'key': 'interfaceId', 'type': 'str'}, + 'component_name': {'key': 'componentName', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'timeout': {'key': 'timeout', 'type': 'int'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'logs': {'key': 'logs', 'type': '[CertificationTaskLog]'}, + 'raw_data': {'key': 'rawData', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinValidationTaskResult, self).__init__(**kwargs) + self.interface_id = kwargs.get('interface_id', None) + self.component_name = kwargs.get('component_name', None) + self.name = kwargs.get('name', None) + self.timeout = kwargs.get('timeout', None) + self.start_time = kwargs.get('start_time', None) + self.end_time = kwargs.get('end_time', None) + self.status = kwargs.get('status', None) + self.message = None + self.logs = None + self.raw_data = None diff --git a/azext_iot/sdk/product/models/digital_twin_validation_task_result_py3.py b/azext_iot/sdk/product/models/digital_twin_validation_task_result_py3.py new file mode 100644 index 000000000..ffb439a8b --- /dev/null +++ b/azext_iot/sdk/product/models/digital_twin_validation_task_result_py3.py @@ -0,0 +1,74 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinValidationTaskResult(Model): + """DigitalTwinValidationTaskResult. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param interface_id: + :type interface_id: str + :param component_name: + :type component_name: str + :param name: + :type name: str + :param timeout: + :type timeout: int + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Created', 'Running', 'Failed', + 'Passed', 'Cancelled', 'Aborted' + :type status: str or ~product.models.enum + :ivar message: + :vartype message: str + :ivar logs: + :vartype logs: list[~product.models.CertificationTaskLog] + :ivar raw_data: + :vartype raw_data: list[str] + """ + + _validation = { + 'message': {'readonly': True}, + 'logs': {'readonly': True}, + 'raw_data': {'readonly': True}, + } + + _attribute_map = { + 'interface_id': {'key': 'interfaceId', 'type': 'str'}, + 'component_name': {'key': 'componentName', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'timeout': {'key': 'timeout', 'type': 'int'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'logs': {'key': 'logs', 'type': '[CertificationTaskLog]'}, + 'raw_data': {'key': 'rawData', 'type': '[str]'}, + } + + def __init__(self, *, interface_id: str=None, component_name: str=None, name: str=None, timeout: int=None, start_time=None, end_time=None, status=None, **kwargs) -> None: + super(DigitalTwinValidationTaskResult, self).__init__(**kwargs) + self.interface_id = interface_id + self.component_name = component_name + self.name = name + self.timeout = timeout + self.start_time = start_time + self.end_time = end_time + self.status = status + self.message = None + self.logs = None + self.raw_data = None diff --git a/azext_iot/sdk/product/models/direct_method_test.py b/azext_iot/sdk/product/models/direct_method_test.py new file mode 100644 index 000000000..1e10ee3fb --- /dev/null +++ b/azext_iot/sdk/product/models/direct_method_test.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DirectMethodTest(Model): + """DirectMethodTest. + + :param method_name: + :type method_name: str + :param payload: + :type payload: str + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'method_name': {'key': 'methodName', 'type': 'str'}, + 'payload': {'key': 'payload', 'type': 'str'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(DirectMethodTest, self).__init__(**kwargs) + self.method_name = kwargs.get('method_name', None) + self.payload = kwargs.get('payload', None) + self.validation_timeout = kwargs.get('validation_timeout', None) + self.is_mandatory = kwargs.get('is_mandatory', None) + self.should_validate = kwargs.get('should_validate', None) diff --git a/azext_iot/sdk/product/models/direct_method_test_py3.py b/azext_iot/sdk/product/models/direct_method_test_py3.py new file mode 100644 index 000000000..71f611c4c --- /dev/null +++ b/azext_iot/sdk/product/models/direct_method_test_py3.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DirectMethodTest(Model): + """DirectMethodTest. + + :param method_name: + :type method_name: str + :param payload: + :type payload: str + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'method_name': {'key': 'methodName', 'type': 'str'}, + 'payload': {'key': 'payload', 'type': 'str'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, *, method_name: str=None, payload: str=None, validation_timeout: int=None, is_mandatory: bool=None, should_validate: bool=None, **kwargs) -> None: + super(DirectMethodTest, self).__init__(**kwargs) + self.method_name = method_name + self.payload = payload + self.validation_timeout = validation_timeout + self.is_mandatory = is_mandatory + self.should_validate = should_validate diff --git a/azext_iot/sdk/product/models/edge_device_validation_task_result.py b/azext_iot/sdk/product/models/edge_device_validation_task_result.py new file mode 100644 index 000000000..b15b30de5 --- /dev/null +++ b/azext_iot/sdk/product/models/edge_device_validation_task_result.py @@ -0,0 +1,66 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EdgeDeviceValidationTaskResult(Model): + """EdgeDeviceValidationTaskResult. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: + :type name: str + :param timeout: + :type timeout: int + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Created', 'Running', 'Failed', + 'Passed', 'Cancelled', 'Aborted' + :type status: str or ~product.models.enum + :ivar message: + :vartype message: str + :ivar logs: + :vartype logs: list[~product.models.CertificationTaskLog] + :ivar raw_data: + :vartype raw_data: list[str] + """ + + _validation = { + 'message': {'readonly': True}, + 'logs': {'readonly': True}, + 'raw_data': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'timeout': {'key': 'timeout', 'type': 'int'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'logs': {'key': 'logs', 'type': '[CertificationTaskLog]'}, + 'raw_data': {'key': 'rawData', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(EdgeDeviceValidationTaskResult, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.timeout = kwargs.get('timeout', None) + self.start_time = kwargs.get('start_time', None) + self.end_time = kwargs.get('end_time', None) + self.status = kwargs.get('status', None) + self.message = None + self.logs = None + self.raw_data = None diff --git a/azext_iot/sdk/product/models/edge_device_validation_task_result_py3.py b/azext_iot/sdk/product/models/edge_device_validation_task_result_py3.py new file mode 100644 index 000000000..bd4535c5b --- /dev/null +++ b/azext_iot/sdk/product/models/edge_device_validation_task_result_py3.py @@ -0,0 +1,66 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EdgeDeviceValidationTaskResult(Model): + """EdgeDeviceValidationTaskResult. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: + :type name: str + :param timeout: + :type timeout: int + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Created', 'Running', 'Failed', + 'Passed', 'Cancelled', 'Aborted' + :type status: str or ~product.models.enum + :ivar message: + :vartype message: str + :ivar logs: + :vartype logs: list[~product.models.CertificationTaskLog] + :ivar raw_data: + :vartype raw_data: list[str] + """ + + _validation = { + 'message': {'readonly': True}, + 'logs': {'readonly': True}, + 'raw_data': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'timeout': {'key': 'timeout', 'type': 'int'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'logs': {'key': 'logs', 'type': '[CertificationTaskLog]'}, + 'raw_data': {'key': 'rawData', 'type': '[str]'}, + } + + def __init__(self, *, name: str=None, timeout: int=None, start_time=None, end_time=None, status=None, **kwargs) -> None: + super(EdgeDeviceValidationTaskResult, self).__init__(**kwargs) + self.name = name + self.timeout = timeout + self.start_time = start_time + self.end_time = end_time + self.status = status + self.message = None + self.logs = None + self.raw_data = None diff --git a/azext_iot/sdk/product/models/enum_schema.py b/azext_iot/sdk/product/models/enum_schema.py new file mode 100644 index 000000000..73745e360 --- /dev/null +++ b/azext_iot/sdk/product/models/enum_schema.py @@ -0,0 +1,64 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EnumSchema(Model): + """EnumSchema. + + :param enum_values: + :type enum_values: list[~product.models.EnumValue] + :param value_schema: Possible values include: 'Unknown', 'Boolean', + 'Date', 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', + 'String', 'Time', 'Enum', 'Object', 'Map', 'Array' + :type value_schema: str or ~product.models.enum + :param schema_type: Possible values include: 'Unknown', 'Boolean', 'Date', + 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', 'String', + 'Time', 'Enum', 'Object', 'Map', 'Array' + :type schema_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'enum_values': {'key': 'enumValues', 'type': '[EnumValue]'}, + 'value_schema': {'key': 'valueSchema', 'type': 'str'}, + 'schema_type': {'key': 'schemaType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(EnumSchema, self).__init__(**kwargs) + self.enum_values = kwargs.get('enum_values', None) + self.value_schema = kwargs.get('value_schema', None) + self.schema_type = kwargs.get('schema_type', None) + self.name = kwargs.get('name', None) + self.comment = kwargs.get('comment', None) + self.display_name = kwargs.get('display_name', None) + self.id = kwargs.get('id', None) + self.description = kwargs.get('description', None) + self.language_version = kwargs.get('language_version', None) diff --git a/azext_iot/sdk/product/models/enum_schema_py3.py b/azext_iot/sdk/product/models/enum_schema_py3.py new file mode 100644 index 000000000..a3e2b7c5f --- /dev/null +++ b/azext_iot/sdk/product/models/enum_schema_py3.py @@ -0,0 +1,64 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EnumSchema(Model): + """EnumSchema. + + :param enum_values: + :type enum_values: list[~product.models.EnumValue] + :param value_schema: Possible values include: 'Unknown', 'Boolean', + 'Date', 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', + 'String', 'Time', 'Enum', 'Object', 'Map', 'Array' + :type value_schema: str or ~product.models.enum + :param schema_type: Possible values include: 'Unknown', 'Boolean', 'Date', + 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', 'String', + 'Time', 'Enum', 'Object', 'Map', 'Array' + :type schema_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'enum_values': {'key': 'enumValues', 'type': '[EnumValue]'}, + 'value_schema': {'key': 'valueSchema', 'type': 'str'}, + 'schema_type': {'key': 'schemaType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, *, enum_values=None, value_schema=None, schema_type=None, name: str=None, comment: str=None, display_name=None, id: str=None, description=None, language_version: int=None, **kwargs) -> None: + super(EnumSchema, self).__init__(**kwargs) + self.enum_values = enum_values + self.value_schema = value_schema + self.schema_type = schema_type + self.name = name + self.comment = comment + self.display_name = display_name + self.id = id + self.description = description + self.language_version = language_version diff --git a/azext_iot/sdk/product/models/enum_value.py b/azext_iot/sdk/product/models/enum_value.py new file mode 100644 index 000000000..cec0a3814 --- /dev/null +++ b/azext_iot/sdk/product/models/enum_value.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EnumValue(Model): + """EnumValue. + + :param value: + :type value: object + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': 'object'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(EnumValue, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.name = kwargs.get('name', None) + self.comment = kwargs.get('comment', None) + self.display_name = kwargs.get('display_name', None) + self.id = kwargs.get('id', None) + self.description = kwargs.get('description', None) + self.language_version = kwargs.get('language_version', None) diff --git a/azext_iot/sdk/product/models/enum_value_py3.py b/azext_iot/sdk/product/models/enum_value_py3.py new file mode 100644 index 000000000..266d3fa87 --- /dev/null +++ b/azext_iot/sdk/product/models/enum_value_py3.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EnumValue(Model): + """EnumValue. + + :param value: + :type value: object + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': 'object'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, *, value=None, name: str=None, comment: str=None, display_name=None, id: str=None, description=None, language_version: int=None, **kwargs) -> None: + super(EnumValue, self).__init__(**kwargs) + self.value = value + self.name = name + self.comment = comment + self.display_name = display_name + self.id = id + self.description = description + self.language_version = language_version diff --git a/azext_iot/sdk/product/models/existing_task_running_conflict_error.py b/azext_iot/sdk/product/models/existing_task_running_conflict_error.py new file mode 100644 index 000000000..1b0bcc535 --- /dev/null +++ b/azext_iot/sdk/product/models/existing_task_running_conflict_error.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ExistingTaskRunningConflictError(Model): + """ExistingTaskRunningConflictError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(ExistingTaskRunningConflictError, self).__init__(**kwargs) + self.message = kwargs.get('message', None) + self.code = kwargs.get('code', None) + self.details = kwargs.get('details', None) diff --git a/azext_iot/sdk/product/models/existing_task_running_conflict_error_py3.py b/azext_iot/sdk/product/models/existing_task_running_conflict_error_py3.py new file mode 100644 index 000000000..c09f32b9a --- /dev/null +++ b/azext_iot/sdk/product/models/existing_task_running_conflict_error_py3.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ExistingTaskRunningConflictError(Model): + """ExistingTaskRunningConflictError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, *, message: str=None, code: int=None, details=None, **kwargs) -> None: + super(ExistingTaskRunningConflictError, self).__init__(**kwargs) + self.message = message + self.code = code + self.details = details diff --git a/azext_iot/sdk/product/models/fail_to_queue_task_error.py b/azext_iot/sdk/product/models/fail_to_queue_task_error.py new file mode 100644 index 000000000..e1fcaa373 --- /dev/null +++ b/azext_iot/sdk/product/models/fail_to_queue_task_error.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class FailToQueueTaskError(Model): + """FailToQueueTaskError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(FailToQueueTaskError, self).__init__(**kwargs) + self.message = kwargs.get('message', None) + self.code = kwargs.get('code', None) + self.details = kwargs.get('details', None) diff --git a/azext_iot/sdk/product/models/fail_to_queue_task_error_py3.py b/azext_iot/sdk/product/models/fail_to_queue_task_error_py3.py new file mode 100644 index 000000000..b81c17a14 --- /dev/null +++ b/azext_iot/sdk/product/models/fail_to_queue_task_error_py3.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class FailToQueueTaskError(Model): + """FailToQueueTaskError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, *, message: str=None, code: int=None, details=None, **kwargs) -> None: + super(FailToQueueTaskError, self).__init__(**kwargs) + self.message = message + self.code = code + self.details = details diff --git a/azext_iot/sdk/product/models/interface_definition_snapshot.py b/azext_iot/sdk/product/models/interface_definition_snapshot.py new file mode 100644 index 000000000..3d9b9affb --- /dev/null +++ b/azext_iot/sdk/product/models/interface_definition_snapshot.py @@ -0,0 +1,37 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class InterfaceDefinitionSnapshot(Model): + """InterfaceDefinitionSnapshot. + + :param interface_id: + :type interface_id: str + :param content: + :type content: str + :param resolution_source: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type resolution_source: str or ~product.models.enum + """ + + _attribute_map = { + 'interface_id': {'key': 'interfaceId', 'type': 'str'}, + 'content': {'key': 'content', 'type': 'str'}, + 'resolution_source': {'key': 'resolutionSource', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(InterfaceDefinitionSnapshot, self).__init__(**kwargs) + self.interface_id = kwargs.get('interface_id', None) + self.content = kwargs.get('content', None) + self.resolution_source = kwargs.get('resolution_source', None) diff --git a/azext_iot/sdk/product/models/interface_definition_snapshot_py3.py b/azext_iot/sdk/product/models/interface_definition_snapshot_py3.py new file mode 100644 index 000000000..25f1b3527 --- /dev/null +++ b/azext_iot/sdk/product/models/interface_definition_snapshot_py3.py @@ -0,0 +1,37 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class InterfaceDefinitionSnapshot(Model): + """InterfaceDefinitionSnapshot. + + :param interface_id: + :type interface_id: str + :param content: + :type content: str + :param resolution_source: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type resolution_source: str or ~product.models.enum + """ + + _attribute_map = { + 'interface_id': {'key': 'interfaceId', 'type': 'str'}, + 'content': {'key': 'content', 'type': 'str'}, + 'resolution_source': {'key': 'resolutionSource', 'type': 'str'}, + } + + def __init__(self, *, interface_id: str=None, content: str=None, resolution_source=None, **kwargs) -> None: + super(InterfaceDefinitionSnapshot, self).__init__(**kwargs) + self.interface_id = interface_id + self.content = content + self.resolution_source = resolution_source diff --git a/azext_iot/sdk/product/models/interface_test.py b/azext_iot/sdk/product/models/interface_test.py new file mode 100644 index 000000000..f9c9d8cda --- /dev/null +++ b/azext_iot/sdk/product/models/interface_test.py @@ -0,0 +1,61 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class InterfaceTest(Model): + """InterfaceTest. + + :param id: + :type id: str + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + :param component_name: + :type component_name: str + :param display_name: + :type display_name: str + :param resolution_source: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type resolution_source: str or ~product.models.enum + :param property_tests: + :type property_tests: list[~product.models.PropertyTest] + :param command_tests: + :type command_tests: list[~product.models.CommandTest] + :param telemetry_tests: + :type telemetry_tests: list[~product.models.TelemetryTest] + """ + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + 'component_name': {'key': 'componentName', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': 'str'}, + 'resolution_source': {'key': 'resolutionSource', 'type': 'str'}, + 'property_tests': {'key': 'propertyTests', 'type': '[PropertyTest]'}, + 'command_tests': {'key': 'commandTests', 'type': '[CommandTest]'}, + 'telemetry_tests': {'key': 'telemetryTests', 'type': '[TelemetryTest]'}, + } + + def __init__(self, **kwargs): + super(InterfaceTest, self).__init__(**kwargs) + self.id = kwargs.get('id', None) + self.is_mandatory = kwargs.get('is_mandatory', None) + self.should_validate = kwargs.get('should_validate', None) + self.component_name = kwargs.get('component_name', None) + self.display_name = kwargs.get('display_name', None) + self.resolution_source = kwargs.get('resolution_source', None) + self.property_tests = kwargs.get('property_tests', None) + self.command_tests = kwargs.get('command_tests', None) + self.telemetry_tests = kwargs.get('telemetry_tests', None) diff --git a/azext_iot/sdk/product/models/interface_test_py3.py b/azext_iot/sdk/product/models/interface_test_py3.py new file mode 100644 index 000000000..c2d7c2d39 --- /dev/null +++ b/azext_iot/sdk/product/models/interface_test_py3.py @@ -0,0 +1,61 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class InterfaceTest(Model): + """InterfaceTest. + + :param id: + :type id: str + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + :param component_name: + :type component_name: str + :param display_name: + :type display_name: str + :param resolution_source: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type resolution_source: str or ~product.models.enum + :param property_tests: + :type property_tests: list[~product.models.PropertyTest] + :param command_tests: + :type command_tests: list[~product.models.CommandTest] + :param telemetry_tests: + :type telemetry_tests: list[~product.models.TelemetryTest] + """ + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + 'component_name': {'key': 'componentName', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': 'str'}, + 'resolution_source': {'key': 'resolutionSource', 'type': 'str'}, + 'property_tests': {'key': 'propertyTests', 'type': '[PropertyTest]'}, + 'command_tests': {'key': 'commandTests', 'type': '[CommandTest]'}, + 'telemetry_tests': {'key': 'telemetryTests', 'type': '[TelemetryTest]'}, + } + + def __init__(self, *, id: str=None, is_mandatory: bool=None, should_validate: bool=None, component_name: str=None, display_name: str=None, resolution_source=None, property_tests=None, command_tests=None, telemetry_tests=None, **kwargs) -> None: + super(InterfaceTest, self).__init__(**kwargs) + self.id = id + self.is_mandatory = is_mandatory + self.should_validate = should_validate + self.component_name = component_name + self.display_name = display_name + self.resolution_source = resolution_source + self.property_tests = property_tests + self.command_tests = command_tests + self.telemetry_tests = telemetry_tests diff --git a/azext_iot/sdk/product/models/iot_device_certification_badge_configuration.py b/azext_iot/sdk/product/models/iot_device_certification_badge_configuration.py new file mode 100644 index 000000000..4569d0967 --- /dev/null +++ b/azext_iot/sdk/product/models/iot_device_certification_badge_configuration.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotDeviceCertificationBadgeConfiguration(Model): + """IotDeviceCertificationBadgeConfiguration. + + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(IotDeviceCertificationBadgeConfiguration, self).__init__(**kwargs) + self.type = kwargs.get('type', None) diff --git a/azext_iot/sdk/product/models/iot_device_certification_badge_configuration_py3.py b/azext_iot/sdk/product/models/iot_device_certification_badge_configuration_py3.py new file mode 100644 index 000000000..747847fdb --- /dev/null +++ b/azext_iot/sdk/product/models/iot_device_certification_badge_configuration_py3.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotDeviceCertificationBadgeConfiguration(Model): + """IotDeviceCertificationBadgeConfiguration. + + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, *, type=None, **kwargs) -> None: + super(IotDeviceCertificationBadgeConfiguration, self).__init__(**kwargs) + self.type = type diff --git a/azext_iot/sdk/product/models/iot_device_certification_badge_result.py b/azext_iot/sdk/product/models/iot_device_certification_badge_result.py new file mode 100644 index 000000000..4e2f18ff9 --- /dev/null +++ b/azext_iot/sdk/product/models/iot_device_certification_badge_result.py @@ -0,0 +1,42 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotDeviceCertificationBadgeResult(Model): + """IotDevice badge certification result. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :vartype type: str or ~product.models.enum + :ivar validation_tasks: + :vartype validation_tasks: + list[~product.models.IotDeviceValidationTaskResult] + """ + + _validation = { + 'type': {'readonly': True}, + 'validation_tasks': {'readonly': True}, + } + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'validation_tasks': {'key': 'validationTasks', 'type': '[IotDeviceValidationTaskResult]'}, + } + + def __init__(self, **kwargs): + super(IotDeviceCertificationBadgeResult, self).__init__(**kwargs) + self.type = None + self.validation_tasks = None diff --git a/azext_iot/sdk/product/models/iot_device_certification_badge_result_py3.py b/azext_iot/sdk/product/models/iot_device_certification_badge_result_py3.py new file mode 100644 index 000000000..f2d4769a3 --- /dev/null +++ b/azext_iot/sdk/product/models/iot_device_certification_badge_result_py3.py @@ -0,0 +1,42 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotDeviceCertificationBadgeResult(Model): + """IotDevice badge certification result. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :vartype type: str or ~product.models.enum + :ivar validation_tasks: + :vartype validation_tasks: + list[~product.models.IotDeviceValidationTaskResult] + """ + + _validation = { + 'type': {'readonly': True}, + 'validation_tasks': {'readonly': True}, + } + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'validation_tasks': {'key': 'validationTasks', 'type': '[IotDeviceValidationTaskResult]'}, + } + + def __init__(self, **kwargs) -> None: + super(IotDeviceCertificationBadgeResult, self).__init__(**kwargs) + self.type = None + self.validation_tasks = None diff --git a/azext_iot/sdk/product/models/iot_device_certification_badge_test_cases.py b/azext_iot/sdk/product/models/iot_device_certification_badge_test_cases.py new file mode 100644 index 000000000..bb587269c --- /dev/null +++ b/azext_iot/sdk/product/models/iot_device_certification_badge_test_cases.py @@ -0,0 +1,45 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotDeviceCertificationBadgeTestCases(Model): + """IotDeviceCertificationBadgeTestCases. + + :param c2_dtests: + :type c2_dtests: list[~product.models.C2DTest] + :param d2_ctests: + :type d2_ctests: list[~product.models.D2CTest] + :param device_twin_tests: + :type device_twin_tests: list[~product.models.DeviceTwinTest] + :param direct_method_tests: + :type direct_method_tests: list[~product.models.DirectMethodTest] + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'c2_dtests': {'key': 'c2DTests', 'type': '[C2DTest]'}, + 'd2_ctests': {'key': 'd2CTests', 'type': '[D2CTest]'}, + 'device_twin_tests': {'key': 'deviceTwinTests', 'type': '[DeviceTwinTest]'}, + 'direct_method_tests': {'key': 'directMethodTests', 'type': '[DirectMethodTest]'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(IotDeviceCertificationBadgeTestCases, self).__init__(**kwargs) + self.c2_dtests = kwargs.get('c2_dtests', None) + self.d2_ctests = kwargs.get('d2_ctests', None) + self.device_twin_tests = kwargs.get('device_twin_tests', None) + self.direct_method_tests = kwargs.get('direct_method_tests', None) + self.type = kwargs.get('type', None) diff --git a/azext_iot/sdk/product/models/iot_device_certification_badge_test_cases_py3.py b/azext_iot/sdk/product/models/iot_device_certification_badge_test_cases_py3.py new file mode 100644 index 000000000..ee3f4fcee --- /dev/null +++ b/azext_iot/sdk/product/models/iot_device_certification_badge_test_cases_py3.py @@ -0,0 +1,45 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotDeviceCertificationBadgeTestCases(Model): + """IotDeviceCertificationBadgeTestCases. + + :param c2_dtests: + :type c2_dtests: list[~product.models.C2DTest] + :param d2_ctests: + :type d2_ctests: list[~product.models.D2CTest] + :param device_twin_tests: + :type device_twin_tests: list[~product.models.DeviceTwinTest] + :param direct_method_tests: + :type direct_method_tests: list[~product.models.DirectMethodTest] + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'c2_dtests': {'key': 'c2DTests', 'type': '[C2DTest]'}, + 'd2_ctests': {'key': 'd2CTests', 'type': '[D2CTest]'}, + 'device_twin_tests': {'key': 'deviceTwinTests', 'type': '[DeviceTwinTest]'}, + 'direct_method_tests': {'key': 'directMethodTests', 'type': '[DirectMethodTest]'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, *, c2_dtests=None, d2_ctests=None, device_twin_tests=None, direct_method_tests=None, type=None, **kwargs) -> None: + super(IotDeviceCertificationBadgeTestCases, self).__init__(**kwargs) + self.c2_dtests = c2_dtests + self.d2_ctests = d2_ctests + self.device_twin_tests = device_twin_tests + self.direct_method_tests = direct_method_tests + self.type = type diff --git a/azext_iot/sdk/product/models/iot_device_validation_task_result.py b/azext_iot/sdk/product/models/iot_device_validation_task_result.py new file mode 100644 index 000000000..6edf3fe29 --- /dev/null +++ b/azext_iot/sdk/product/models/iot_device_validation_task_result.py @@ -0,0 +1,66 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotDeviceValidationTaskResult(Model): + """IotDeviceValidationTaskResult. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: + :type name: str + :param timeout: + :type timeout: int + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Created', 'Running', 'Failed', + 'Passed', 'Cancelled', 'Aborted' + :type status: str or ~product.models.enum + :ivar message: + :vartype message: str + :ivar logs: + :vartype logs: list[~product.models.CertificationTaskLog] + :ivar raw_data: + :vartype raw_data: list[str] + """ + + _validation = { + 'message': {'readonly': True}, + 'logs': {'readonly': True}, + 'raw_data': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'timeout': {'key': 'timeout', 'type': 'int'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'logs': {'key': 'logs', 'type': '[CertificationTaskLog]'}, + 'raw_data': {'key': 'rawData', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(IotDeviceValidationTaskResult, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.timeout = kwargs.get('timeout', None) + self.start_time = kwargs.get('start_time', None) + self.end_time = kwargs.get('end_time', None) + self.status = kwargs.get('status', None) + self.message = None + self.logs = None + self.raw_data = None diff --git a/azext_iot/sdk/product/models/iot_device_validation_task_result_py3.py b/azext_iot/sdk/product/models/iot_device_validation_task_result_py3.py new file mode 100644 index 000000000..f1d979640 --- /dev/null +++ b/azext_iot/sdk/product/models/iot_device_validation_task_result_py3.py @@ -0,0 +1,66 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotDeviceValidationTaskResult(Model): + """IotDeviceValidationTaskResult. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: + :type name: str + :param timeout: + :type timeout: int + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Created', 'Running', 'Failed', + 'Passed', 'Cancelled', 'Aborted' + :type status: str or ~product.models.enum + :ivar message: + :vartype message: str + :ivar logs: + :vartype logs: list[~product.models.CertificationTaskLog] + :ivar raw_data: + :vartype raw_data: list[str] + """ + + _validation = { + 'message': {'readonly': True}, + 'logs': {'readonly': True}, + 'raw_data': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'timeout': {'key': 'timeout', 'type': 'int'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'logs': {'key': 'logs', 'type': '[CertificationTaskLog]'}, + 'raw_data': {'key': 'rawData', 'type': '[str]'}, + } + + def __init__(self, *, name: str=None, timeout: int=None, start_time=None, end_time=None, status=None, **kwargs) -> None: + super(IotDeviceValidationTaskResult, self).__init__(**kwargs) + self.name = name + self.timeout = timeout + self.start_time = start_time + self.end_time = end_time + self.status = status + self.message = None + self.logs = None + self.raw_data = None diff --git a/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_configuration.py b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_configuration.py new file mode 100644 index 000000000..d815245dc --- /dev/null +++ b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_configuration.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotEdgeCompatibleCertificationBadgeConfiguration(Model): + """IotEdgeCompatibleCertificationBadgeConfiguration. + + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(IotEdgeCompatibleCertificationBadgeConfiguration, self).__init__(**kwargs) + self.type = kwargs.get('type', None) diff --git a/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_configuration_py3.py b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_configuration_py3.py new file mode 100644 index 000000000..9e6003a5c --- /dev/null +++ b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_configuration_py3.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotEdgeCompatibleCertificationBadgeConfiguration(Model): + """IotEdgeCompatibleCertificationBadgeConfiguration. + + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, *, type=None, **kwargs) -> None: + super(IotEdgeCompatibleCertificationBadgeConfiguration, self).__init__(**kwargs) + self.type = type diff --git a/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_result.py b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_result.py new file mode 100644 index 000000000..7648fd326 --- /dev/null +++ b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_result.py @@ -0,0 +1,42 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotEdgeCompatibleCertificationBadgeResult(Model): + """IotDevice badge certification result. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :vartype type: str or ~product.models.enum + :ivar validation_tasks: + :vartype validation_tasks: + list[~product.models.EdgeDeviceValidationTaskResult] + """ + + _validation = { + 'type': {'readonly': True}, + 'validation_tasks': {'readonly': True}, + } + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'validation_tasks': {'key': 'validationTasks', 'type': '[EdgeDeviceValidationTaskResult]'}, + } + + def __init__(self, **kwargs): + super(IotEdgeCompatibleCertificationBadgeResult, self).__init__(**kwargs) + self.type = None + self.validation_tasks = None diff --git a/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_result_py3.py b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_result_py3.py new file mode 100644 index 000000000..7c2c1580b --- /dev/null +++ b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_result_py3.py @@ -0,0 +1,42 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotEdgeCompatibleCertificationBadgeResult(Model): + """IotDevice badge certification result. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :vartype type: str or ~product.models.enum + :ivar validation_tasks: + :vartype validation_tasks: + list[~product.models.EdgeDeviceValidationTaskResult] + """ + + _validation = { + 'type': {'readonly': True}, + 'validation_tasks': {'readonly': True}, + } + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'validation_tasks': {'key': 'validationTasks', 'type': '[EdgeDeviceValidationTaskResult]'}, + } + + def __init__(self, **kwargs) -> None: + super(IotEdgeCompatibleCertificationBadgeResult, self).__init__(**kwargs) + self.type = None + self.validation_tasks = None diff --git a/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_test_cases.py b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_test_cases.py new file mode 100644 index 000000000..4e2a3cf27 --- /dev/null +++ b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_test_cases.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotEdgeCompatibleCertificationBadgeTestCases(Model): + """IotEdgeCompatibleCertificationBadgeTestCases. + + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(IotEdgeCompatibleCertificationBadgeTestCases, self).__init__(**kwargs) + self.type = kwargs.get('type', None) diff --git a/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_test_cases_py3.py b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_test_cases_py3.py new file mode 100644 index 000000000..ecff69557 --- /dev/null +++ b/azext_iot/sdk/product/models/iot_edge_compatible_certification_badge_test_cases_py3.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IotEdgeCompatibleCertificationBadgeTestCases(Model): + """IotEdgeCompatibleCertificationBadgeTestCases. + + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, *, type=None, **kwargs) -> None: + super(IotEdgeCompatibleCertificationBadgeTestCases, self).__init__(**kwargs) + self.type = type diff --git a/azext_iot/sdk/product/models/map_schema.py b/azext_iot/sdk/product/models/map_schema.py new file mode 100644 index 000000000..f9697fdbd --- /dev/null +++ b/azext_iot/sdk/product/models/map_schema.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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class MapSchema(Model): + """MapSchema. + + :param key_schema: + :type key_schema: ~product.models.SchemaField + :param value_schema: + :type value_schema: ~product.models.SchemaField + :param schema_type: Possible values include: 'Unknown', 'Boolean', 'Date', + 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', 'String', + 'Time', 'Enum', 'Object', 'Map', 'Array' + :type schema_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'key_schema': {'key': 'keySchema', 'type': 'SchemaField'}, + 'value_schema': {'key': 'valueSchema', 'type': 'SchemaField'}, + 'schema_type': {'key': 'schemaType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(MapSchema, self).__init__(**kwargs) + self.key_schema = kwargs.get('key_schema', None) + self.value_schema = kwargs.get('value_schema', None) + self.schema_type = kwargs.get('schema_type', None) + self.name = kwargs.get('name', None) + self.comment = kwargs.get('comment', None) + self.display_name = kwargs.get('display_name', None) + self.id = kwargs.get('id', None) + self.description = kwargs.get('description', None) + self.language_version = kwargs.get('language_version', None) diff --git a/azext_iot/sdk/product/models/map_schema_py3.py b/azext_iot/sdk/product/models/map_schema_py3.py new file mode 100644 index 000000000..4875fba99 --- /dev/null +++ b/azext_iot/sdk/product/models/map_schema_py3.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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class MapSchema(Model): + """MapSchema. + + :param key_schema: + :type key_schema: ~product.models.SchemaField + :param value_schema: + :type value_schema: ~product.models.SchemaField + :param schema_type: Possible values include: 'Unknown', 'Boolean', 'Date', + 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', 'String', + 'Time', 'Enum', 'Object', 'Map', 'Array' + :type schema_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'key_schema': {'key': 'keySchema', 'type': 'SchemaField'}, + 'value_schema': {'key': 'valueSchema', 'type': 'SchemaField'}, + 'schema_type': {'key': 'schemaType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, *, key_schema=None, value_schema=None, schema_type=None, name: str=None, comment: str=None, display_name=None, id: str=None, description=None, language_version: int=None, **kwargs) -> None: + super(MapSchema, self).__init__(**kwargs) + self.key_schema = key_schema + self.value_schema = value_schema + self.schema_type = schema_type + self.name = name + self.comment = comment + self.display_name = display_name + self.id = id + self.description = description + self.language_version = language_version diff --git a/azext_iot/sdk/product/models/model_definition_snapshot.py b/azext_iot/sdk/product/models/model_definition_snapshot.py new file mode 100644 index 000000000..cd259a63e --- /dev/null +++ b/azext_iot/sdk/product/models/model_definition_snapshot.py @@ -0,0 +1,42 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelDefinitionSnapshot(Model): + """ModelDefinitionSnapshot. + + :param id: + :type id: str + :param content: + :type content: str + :param resolution_source: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type resolution_source: str or ~product.models.enum + :param interface_snapshots: + :type interface_snapshots: + list[~product.models.InterfaceDefinitionSnapshot] + """ + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'content': {'key': 'content', 'type': 'str'}, + 'resolution_source': {'key': 'resolutionSource', 'type': 'str'}, + 'interface_snapshots': {'key': 'interfaceSnapshots', 'type': '[InterfaceDefinitionSnapshot]'}, + } + + def __init__(self, **kwargs): + super(ModelDefinitionSnapshot, self).__init__(**kwargs) + self.id = kwargs.get('id', None) + self.content = kwargs.get('content', None) + self.resolution_source = kwargs.get('resolution_source', None) + self.interface_snapshots = kwargs.get('interface_snapshots', None) diff --git a/azext_iot/sdk/product/models/model_definition_snapshot_py3.py b/azext_iot/sdk/product/models/model_definition_snapshot_py3.py new file mode 100644 index 000000000..bc3b18a26 --- /dev/null +++ b/azext_iot/sdk/product/models/model_definition_snapshot_py3.py @@ -0,0 +1,42 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelDefinitionSnapshot(Model): + """ModelDefinitionSnapshot. + + :param id: + :type id: str + :param content: + :type content: str + :param resolution_source: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type resolution_source: str or ~product.models.enum + :param interface_snapshots: + :type interface_snapshots: + list[~product.models.InterfaceDefinitionSnapshot] + """ + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'content': {'key': 'content', 'type': 'str'}, + 'resolution_source': {'key': 'resolutionSource', 'type': 'str'}, + 'interface_snapshots': {'key': 'interfaceSnapshots', 'type': '[InterfaceDefinitionSnapshot]'}, + } + + def __init__(self, *, id: str=None, content: str=None, resolution_source=None, interface_snapshots=None, **kwargs) -> None: + super(ModelDefinitionSnapshot, self).__init__(**kwargs) + self.id = id + self.content = content + self.resolution_source = resolution_source + self.interface_snapshots = interface_snapshots diff --git a/azext_iot/sdk/product/models/model_resolution_failure_error.py b/azext_iot/sdk/product/models/model_resolution_failure_error.py new file mode 100644 index 000000000..cf6738ad6 --- /dev/null +++ b/azext_iot/sdk/product/models/model_resolution_failure_error.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelResolutionFailureError(Model): + """ModelResolutionFailureError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(ModelResolutionFailureError, self).__init__(**kwargs) + self.message = kwargs.get('message', None) + self.code = kwargs.get('code', None) + self.details = kwargs.get('details', None) diff --git a/azext_iot/sdk/product/models/model_resolution_failure_error_py3.py b/azext_iot/sdk/product/models/model_resolution_failure_error_py3.py new file mode 100644 index 000000000..7ed5cfc5b --- /dev/null +++ b/azext_iot/sdk/product/models/model_resolution_failure_error_py3.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelResolutionFailureError(Model): + """ModelResolutionFailureError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, *, message: str=None, code: int=None, details=None, **kwargs) -> None: + super(ModelResolutionFailureError, self).__init__(**kwargs) + self.message = message + self.code = code + self.details = details diff --git a/azext_iot/sdk/product/models/model_resolution_source.py b/azext_iot/sdk/product/models/model_resolution_source.py new file mode 100644 index 000000000..a2ace37c4 --- /dev/null +++ b/azext_iot/sdk/product/models/model_resolution_source.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelResolutionSource(Model): + """ModelResolutionSource. + + :param priority: + :type priority: int + :param source_type: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type source_type: str or ~product.models.enum + """ + + _attribute_map = { + 'priority': {'key': 'priority', 'type': 'int'}, + 'source_type': {'key': 'sourceType', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ModelResolutionSource, self).__init__(**kwargs) + self.priority = kwargs.get('priority', None) + self.source_type = kwargs.get('source_type', None) diff --git a/azext_iot/sdk/product/models/model_resolution_source_py3.py b/azext_iot/sdk/product/models/model_resolution_source_py3.py new file mode 100644 index 000000000..e556ef5af --- /dev/null +++ b/azext_iot/sdk/product/models/model_resolution_source_py3.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ModelResolutionSource(Model): + """ModelResolutionSource. + + :param priority: + :type priority: int + :param source_type: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type source_type: str or ~product.models.enum + """ + + _attribute_map = { + 'priority': {'key': 'priority', 'type': 'int'}, + 'source_type': {'key': 'sourceType', 'type': 'str'}, + } + + def __init__(self, *, priority: int=None, source_type=None, **kwargs) -> None: + super(ModelResolutionSource, self).__init__(**kwargs) + self.priority = priority + self.source_type = source_type diff --git a/azext_iot/sdk/product/models/new_task_payload.py b/azext_iot/sdk/product/models/new_task_payload.py new file mode 100644 index 000000000..6027b9c79 --- /dev/null +++ b/azext_iot/sdk/product/models/new_task_payload.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class NewTaskPayload(Model): + """NewTaskPayload. + + :param task_type: Possible values include: 'QueueTestRun', + 'GenerateTestCases' + :type task_type: str or ~product.models.enum + """ + + _attribute_map = { + 'task_type': {'key': 'taskType', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(NewTaskPayload, self).__init__(**kwargs) + self.task_type = kwargs.get('task_type', None) diff --git a/azext_iot/sdk/product/models/new_task_payload_py3.py b/azext_iot/sdk/product/models/new_task_payload_py3.py new file mode 100644 index 000000000..43a4bdb9b --- /dev/null +++ b/azext_iot/sdk/product/models/new_task_payload_py3.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class NewTaskPayload(Model): + """NewTaskPayload. + + :param task_type: Possible values include: 'QueueTestRun', + 'GenerateTestCases' + :type task_type: str or ~product.models.enum + """ + + _attribute_map = { + 'task_type': {'key': 'taskType', 'type': 'str'}, + } + + def __init__(self, *, task_type=None, **kwargs) -> None: + super(NewTaskPayload, self).__init__(**kwargs) + self.task_type = task_type diff --git a/azext_iot/sdk/product/models/object_schema.py b/azext_iot/sdk/product/models/object_schema.py new file mode 100644 index 000000000..44328637b --- /dev/null +++ b/azext_iot/sdk/product/models/object_schema.py @@ -0,0 +1,58 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ObjectSchema(Model): + """ObjectSchema. + + :param fields: + :type fields: list[~product.models.SchemaField] + :param schema_type: Possible values include: 'Unknown', 'Boolean', 'Date', + 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', 'String', + 'Time', 'Enum', 'Object', 'Map', 'Array' + :type schema_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'fields': {'key': 'fields', 'type': '[SchemaField]'}, + 'schema_type': {'key': 'schemaType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(ObjectSchema, self).__init__(**kwargs) + self.fields = kwargs.get('fields', None) + self.schema_type = kwargs.get('schema_type', None) + self.name = kwargs.get('name', None) + self.comment = kwargs.get('comment', None) + self.display_name = kwargs.get('display_name', None) + self.id = kwargs.get('id', None) + self.description = kwargs.get('description', None) + self.language_version = kwargs.get('language_version', None) diff --git a/azext_iot/sdk/product/models/object_schema_py3.py b/azext_iot/sdk/product/models/object_schema_py3.py new file mode 100644 index 000000000..af02fbc61 --- /dev/null +++ b/azext_iot/sdk/product/models/object_schema_py3.py @@ -0,0 +1,58 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ObjectSchema(Model): + """ObjectSchema. + + :param fields: + :type fields: list[~product.models.SchemaField] + :param schema_type: Possible values include: 'Unknown', 'Boolean', 'Date', + 'DateTime', 'Double', 'Duration', 'Float', 'Integer', 'Long', 'String', + 'Time', 'Enum', 'Object', 'Map', 'Array' + :type schema_type: str or ~product.models.enum + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'fields': {'key': 'fields', 'type': '[SchemaField]'}, + 'schema_type': {'key': 'schemaType', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, *, fields=None, schema_type=None, name: str=None, comment: str=None, display_name=None, id: str=None, description=None, language_version: int=None, **kwargs) -> None: + super(ObjectSchema, self).__init__(**kwargs) + self.fields = fields + self.schema_type = schema_type + self.name = name + self.comment = comment + self.display_name = display_name + self.id = id + self.description = description + self.language_version = language_version diff --git a/azext_iot/sdk/product/models/pnp_certification_badge_configuration.py b/azext_iot/sdk/product/models/pnp_certification_badge_configuration.py new file mode 100644 index 000000000..a8b3b7087 --- /dev/null +++ b/azext_iot/sdk/product/models/pnp_certification_badge_configuration.py @@ -0,0 +1,37 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PnpCertificationBadgeConfiguration(Model): + """PnpCertificationBadgeConfiguration. + + :param digital_twin_model_definitions: + :type digital_twin_model_definitions: list[str] + :param resolution_sources: + :type resolution_sources: list[~product.models.ModelResolutionSource] + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'digital_twin_model_definitions': {'key': 'digitalTwinModelDefinitions', 'type': '[str]'}, + 'resolution_sources': {'key': 'resolutionSources', 'type': '[ModelResolutionSource]'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(PnpCertificationBadgeConfiguration, self).__init__(**kwargs) + self.digital_twin_model_definitions = kwargs.get('digital_twin_model_definitions', None) + self.resolution_sources = kwargs.get('resolution_sources', None) + self.type = kwargs.get('type', None) diff --git a/azext_iot/sdk/product/models/pnp_certification_badge_configuration_py3.py b/azext_iot/sdk/product/models/pnp_certification_badge_configuration_py3.py new file mode 100644 index 000000000..b18fad109 --- /dev/null +++ b/azext_iot/sdk/product/models/pnp_certification_badge_configuration_py3.py @@ -0,0 +1,37 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PnpCertificationBadgeConfiguration(Model): + """PnpCertificationBadgeConfiguration. + + :param digital_twin_model_definitions: + :type digital_twin_model_definitions: list[str] + :param resolution_sources: + :type resolution_sources: list[~product.models.ModelResolutionSource] + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'digital_twin_model_definitions': {'key': 'digitalTwinModelDefinitions', 'type': '[str]'}, + 'resolution_sources': {'key': 'resolutionSources', 'type': '[ModelResolutionSource]'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, *, digital_twin_model_definitions=None, resolution_sources=None, type=None, **kwargs) -> None: + super(PnpCertificationBadgeConfiguration, self).__init__(**kwargs) + self.digital_twin_model_definitions = digital_twin_model_definitions + self.resolution_sources = resolution_sources + self.type = type diff --git a/azext_iot/sdk/product/models/pnp_certification_badge_result.py b/azext_iot/sdk/product/models/pnp_certification_badge_result.py new file mode 100644 index 000000000..941ed3180 --- /dev/null +++ b/azext_iot/sdk/product/models/pnp_certification_badge_result.py @@ -0,0 +1,53 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PnpCertificationBadgeResult(Model): + """Pnp badge certification result. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar validation_tasks: + :vartype validation_tasks: + list[~product.models.DigitalTwinValidationTaskResult] + :ivar pre_validation_tasks: + :vartype pre_validation_tasks: + list[~product.models.PreValidationTaskResult] + :ivar type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :vartype type: str or ~product.models.enum + :param resolution_source: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type resolution_source: str or ~product.models.enum + """ + + _validation = { + 'validation_tasks': {'readonly': True}, + 'pre_validation_tasks': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'validation_tasks': {'key': 'validationTasks', 'type': '[DigitalTwinValidationTaskResult]'}, + 'pre_validation_tasks': {'key': 'preValidationTasks', 'type': '[PreValidationTaskResult]'}, + 'type': {'key': 'type', 'type': 'str'}, + 'resolution_source': {'key': 'resolutionSource', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(PnpCertificationBadgeResult, self).__init__(**kwargs) + self.validation_tasks = None + self.pre_validation_tasks = None + self.type = None + self.resolution_source = kwargs.get('resolution_source', None) diff --git a/azext_iot/sdk/product/models/pnp_certification_badge_result_py3.py b/azext_iot/sdk/product/models/pnp_certification_badge_result_py3.py new file mode 100644 index 000000000..cf6668ecd --- /dev/null +++ b/azext_iot/sdk/product/models/pnp_certification_badge_result_py3.py @@ -0,0 +1,53 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PnpCertificationBadgeResult(Model): + """Pnp badge certification result. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar validation_tasks: + :vartype validation_tasks: + list[~product.models.DigitalTwinValidationTaskResult] + :ivar pre_validation_tasks: + :vartype pre_validation_tasks: + list[~product.models.PreValidationTaskResult] + :ivar type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :vartype type: str or ~product.models.enum + :param resolution_source: Possible values include: 'Unknown', + 'GlobalRepository', 'PrivateRepository', 'UserUploads' + :type resolution_source: str or ~product.models.enum + """ + + _validation = { + 'validation_tasks': {'readonly': True}, + 'pre_validation_tasks': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'validation_tasks': {'key': 'validationTasks', 'type': '[DigitalTwinValidationTaskResult]'}, + 'pre_validation_tasks': {'key': 'preValidationTasks', 'type': '[PreValidationTaskResult]'}, + 'type': {'key': 'type', 'type': 'str'}, + 'resolution_source': {'key': 'resolutionSource', 'type': 'str'}, + } + + def __init__(self, *, resolution_source=None, **kwargs) -> None: + super(PnpCertificationBadgeResult, self).__init__(**kwargs) + self.validation_tasks = None + self.pre_validation_tasks = None + self.type = None + self.resolution_source = resolution_source diff --git a/azext_iot/sdk/product/models/pnp_certification_badge_test_cases.py b/azext_iot/sdk/product/models/pnp_certification_badge_test_cases.py new file mode 100644 index 000000000..0f0f0aa27 --- /dev/null +++ b/azext_iot/sdk/product/models/pnp_certification_badge_test_cases.py @@ -0,0 +1,37 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PnpCertificationBadgeTestCases(Model): + """PnpCertificationBadgeTestCases. + + :param model_definition_snapshot: + :type model_definition_snapshot: ~product.models.ModelDefinitionSnapshot + :param interface_tests: + :type interface_tests: list[~product.models.InterfaceTest] + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'model_definition_snapshot': {'key': 'modelDefinitionSnapshot', 'type': 'ModelDefinitionSnapshot'}, + 'interface_tests': {'key': 'interfaceTests', 'type': '[InterfaceTest]'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(PnpCertificationBadgeTestCases, self).__init__(**kwargs) + self.model_definition_snapshot = kwargs.get('model_definition_snapshot', None) + self.interface_tests = kwargs.get('interface_tests', None) + self.type = kwargs.get('type', None) diff --git a/azext_iot/sdk/product/models/pnp_certification_badge_test_cases_py3.py b/azext_iot/sdk/product/models/pnp_certification_badge_test_cases_py3.py new file mode 100644 index 000000000..984f21e58 --- /dev/null +++ b/azext_iot/sdk/product/models/pnp_certification_badge_test_cases_py3.py @@ -0,0 +1,37 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PnpCertificationBadgeTestCases(Model): + """PnpCertificationBadgeTestCases. + + :param model_definition_snapshot: + :type model_definition_snapshot: ~product.models.ModelDefinitionSnapshot + :param interface_tests: + :type interface_tests: list[~product.models.InterfaceTest] + :param type: Possible values include: 'IotDevice', 'Pnp', + 'IotEdgeCompatible' + :type type: str or ~product.models.enum + """ + + _attribute_map = { + 'model_definition_snapshot': {'key': 'modelDefinitionSnapshot', 'type': 'ModelDefinitionSnapshot'}, + 'interface_tests': {'key': 'interfaceTests', 'type': '[InterfaceTest]'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, *, model_definition_snapshot=None, interface_tests=None, type=None, **kwargs) -> None: + super(PnpCertificationBadgeTestCases, self).__init__(**kwargs) + self.model_definition_snapshot = model_definition_snapshot + self.interface_tests = interface_tests + self.type = type diff --git a/azext_iot/sdk/product/models/pre_validation_task_result.py b/azext_iot/sdk/product/models/pre_validation_task_result.py new file mode 100644 index 000000000..d0f87dd10 --- /dev/null +++ b/azext_iot/sdk/product/models/pre_validation_task_result.py @@ -0,0 +1,57 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PreValidationTaskResult(Model): + """PreValidationTaskResult. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: + :type name: str + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Created', 'Running', 'Failed', + 'Passed', 'Cancelled', 'Aborted' + :type status: str or ~product.models.enum + :ivar logs: + :vartype logs: list[~product.models.CertificationTaskLog] + :ivar raw_data: + :vartype raw_data: list[str] + """ + + _validation = { + 'logs': {'readonly': True}, + 'raw_data': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'logs': {'key': 'logs', 'type': '[CertificationTaskLog]'}, + 'raw_data': {'key': 'rawData', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(PreValidationTaskResult, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.start_time = kwargs.get('start_time', None) + self.end_time = kwargs.get('end_time', None) + self.status = kwargs.get('status', None) + self.logs = None + self.raw_data = None diff --git a/azext_iot/sdk/product/models/pre_validation_task_result_py3.py b/azext_iot/sdk/product/models/pre_validation_task_result_py3.py new file mode 100644 index 000000000..2dd376ba8 --- /dev/null +++ b/azext_iot/sdk/product/models/pre_validation_task_result_py3.py @@ -0,0 +1,57 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PreValidationTaskResult(Model): + """PreValidationTaskResult. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: + :type name: str + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Created', 'Running', 'Failed', + 'Passed', 'Cancelled', 'Aborted' + :type status: str or ~product.models.enum + :ivar logs: + :vartype logs: list[~product.models.CertificationTaskLog] + :ivar raw_data: + :vartype raw_data: list[str] + """ + + _validation = { + 'logs': {'readonly': True}, + 'raw_data': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'logs': {'key': 'logs', 'type': '[CertificationTaskLog]'}, + 'raw_data': {'key': 'rawData', 'type': '[str]'}, + } + + def __init__(self, *, name: str=None, start_time=None, end_time=None, status=None, **kwargs) -> None: + super(PreValidationTaskResult, self).__init__(**kwargs) + self.name = name + self.start_time = start_time + self.end_time = end_time + self.status = status + self.logs = None + self.raw_data = None diff --git a/azext_iot/sdk/product/models/property_model.py b/azext_iot/sdk/product/models/property_model.py new file mode 100644 index 000000000..3f06422df --- /dev/null +++ b/azext_iot/sdk/product/models/property_model.py @@ -0,0 +1,64 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PropertyModel(Model): + """PropertyModel. + + :param supplemental_types: + :type supplemental_types: list[str] + :param schema: + :type schema: object + :param writable: + :type writable: bool + :param supplemental_properties: + :type supplemental_properties: dict[str, object] + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'supplemental_types': {'key': 'supplementalTypes', 'type': '[str]'}, + 'schema': {'key': 'schema', 'type': 'object'}, + 'writable': {'key': 'writable', 'type': 'bool'}, + 'supplemental_properties': {'key': 'supplementalProperties', 'type': '{object}'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(PropertyModel, self).__init__(**kwargs) + self.supplemental_types = kwargs.get('supplemental_types', None) + self.schema = kwargs.get('schema', None) + self.writable = kwargs.get('writable', None) + self.supplemental_properties = kwargs.get('supplemental_properties', None) + self.name = kwargs.get('name', None) + self.comment = kwargs.get('comment', None) + self.display_name = kwargs.get('display_name', None) + self.id = kwargs.get('id', None) + self.description = kwargs.get('description', None) + self.language_version = kwargs.get('language_version', None) diff --git a/azext_iot/sdk/product/models/property_model_py3.py b/azext_iot/sdk/product/models/property_model_py3.py new file mode 100644 index 000000000..7c981a5d5 --- /dev/null +++ b/azext_iot/sdk/product/models/property_model_py3.py @@ -0,0 +1,64 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PropertyModel(Model): + """PropertyModel. + + :param supplemental_types: + :type supplemental_types: list[str] + :param schema: + :type schema: object + :param writable: + :type writable: bool + :param supplemental_properties: + :type supplemental_properties: dict[str, object] + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'supplemental_types': {'key': 'supplementalTypes', 'type': '[str]'}, + 'schema': {'key': 'schema', 'type': 'object'}, + 'writable': {'key': 'writable', 'type': 'bool'}, + 'supplemental_properties': {'key': 'supplementalProperties', 'type': '{object}'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, *, supplemental_types=None, schema=None, writable: bool=None, supplemental_properties=None, name: str=None, comment: str=None, display_name=None, id: str=None, description=None, language_version: int=None, **kwargs) -> None: + super(PropertyModel, self).__init__(**kwargs) + self.supplemental_types = supplemental_types + self.schema = schema + self.writable = writable + self.supplemental_properties = supplemental_properties + self.name = name + self.comment = comment + self.display_name = display_name + self.id = id + self.description = description + self.language_version = language_version diff --git a/azext_iot/sdk/product/models/property_test.py b/azext_iot/sdk/product/models/property_test.py new file mode 100644 index 000000000..8e96a1d24 --- /dev/null +++ b/azext_iot/sdk/product/models/property_test.py @@ -0,0 +1,48 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PropertyTest(Model): + """PropertyTest. + + :param property: + :type property: ~product.models.PropertyModel + :param value_to_write: + :type value_to_write: str + :param value_to_report: + :type value_to_report: str + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'property': {'key': 'property', 'type': 'PropertyModel'}, + 'value_to_write': {'key': 'valueToWrite', 'type': 'str'}, + 'value_to_report': {'key': 'valueToReport', 'type': 'str'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(PropertyTest, self).__init__(**kwargs) + self.property = kwargs.get('property', None) + self.value_to_write = kwargs.get('value_to_write', None) + self.value_to_report = kwargs.get('value_to_report', None) + self.validation_timeout = kwargs.get('validation_timeout', None) + self.is_mandatory = kwargs.get('is_mandatory', None) + self.should_validate = kwargs.get('should_validate', None) diff --git a/azext_iot/sdk/product/models/property_test_py3.py b/azext_iot/sdk/product/models/property_test_py3.py new file mode 100644 index 000000000..1143d1e51 --- /dev/null +++ b/azext_iot/sdk/product/models/property_test_py3.py @@ -0,0 +1,48 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PropertyTest(Model): + """PropertyTest. + + :param property: + :type property: ~product.models.PropertyModel + :param value_to_write: + :type value_to_write: str + :param value_to_report: + :type value_to_report: str + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'property': {'key': 'property', 'type': 'PropertyModel'}, + 'value_to_write': {'key': 'valueToWrite', 'type': 'str'}, + 'value_to_report': {'key': 'valueToReport', 'type': 'str'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, *, property=None, value_to_write: str=None, value_to_report: str=None, validation_timeout: int=None, is_mandatory: bool=None, should_validate: bool=None, **kwargs) -> None: + super(PropertyTest, self).__init__(**kwargs) + self.property = property + self.value_to_write = value_to_write + self.value_to_report = value_to_report + self.validation_timeout = validation_timeout + self.is_mandatory = is_mandatory + self.should_validate = should_validate diff --git a/azext_iot/sdk/product/models/provisioning_configuration.py b/azext_iot/sdk/product/models/provisioning_configuration.py new file mode 100644 index 000000000..e11c3c326 --- /dev/null +++ b/azext_iot/sdk/product/models/provisioning_configuration.py @@ -0,0 +1,60 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ProvisioningConfiguration(Model): + """ProvisioningConfiguration. + + :param type: Possible values include: 'ConnectionString', 'X509', 'TPM', + 'SymmetricKey' + :type type: str or ~product.models.enum + :param device_id: + :type device_id: str + :param dps_registration_id: + :type dps_registration_id: str + :param region: Possible values include: 'Unknown', 'CentralUS', + 'CentralUSEUAP', 'EastUS', 'EastUS2EUAP', 'JapanEast', 'NorthEurope', + 'WestUS', 'WestUS2' + :type region: str or ~product.models.enum + :param device_connection_string: + :type device_connection_string: str + :param x509_enrollment_information: + :type x509_enrollment_information: ~product.models.X509Enrollment + :param symmetric_key_enrollment_information: + :type symmetric_key_enrollment_information: + ~product.models.SymmetricKeyEnrollment + :param tpm_enrollment_information: + :type tpm_enrollment_information: ~product.models.TpmEnrollment + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'device_id': {'key': 'deviceId', 'type': 'str'}, + 'dps_registration_id': {'key': 'dpsRegistrationId', 'type': 'str'}, + 'region': {'key': 'region', 'type': 'str'}, + 'device_connection_string': {'key': 'deviceConnectionString', 'type': 'str'}, + 'x509_enrollment_information': {'key': 'x509EnrollmentInformation', 'type': 'X509Enrollment'}, + 'symmetric_key_enrollment_information': {'key': 'symmetricKeyEnrollmentInformation', 'type': 'SymmetricKeyEnrollment'}, + 'tpm_enrollment_information': {'key': 'tpmEnrollmentInformation', 'type': 'TpmEnrollment'}, + } + + def __init__(self, **kwargs): + super(ProvisioningConfiguration, self).__init__(**kwargs) + self.type = kwargs.get('type', None) + self.device_id = kwargs.get('device_id', None) + self.dps_registration_id = kwargs.get('dps_registration_id', None) + self.region = kwargs.get('region', None) + self.device_connection_string = kwargs.get('device_connection_string', None) + self.x509_enrollment_information = kwargs.get('x509_enrollment_information', None) + self.symmetric_key_enrollment_information = kwargs.get('symmetric_key_enrollment_information', None) + self.tpm_enrollment_information = kwargs.get('tpm_enrollment_information', None) diff --git a/azext_iot/sdk/product/models/provisioning_configuration_py3.py b/azext_iot/sdk/product/models/provisioning_configuration_py3.py new file mode 100644 index 000000000..6482c4209 --- /dev/null +++ b/azext_iot/sdk/product/models/provisioning_configuration_py3.py @@ -0,0 +1,60 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ProvisioningConfiguration(Model): + """ProvisioningConfiguration. + + :param type: Possible values include: 'ConnectionString', 'X509', 'TPM', + 'SymmetricKey' + :type type: str or ~product.models.enum + :param device_id: + :type device_id: str + :param dps_registration_id: + :type dps_registration_id: str + :param region: Possible values include: 'Unknown', 'CentralUS', + 'CentralUSEUAP', 'EastUS', 'EastUS2EUAP', 'JapanEast', 'NorthEurope', + 'WestUS', 'WestUS2' + :type region: str or ~product.models.enum + :param device_connection_string: + :type device_connection_string: str + :param x509_enrollment_information: + :type x509_enrollment_information: ~product.models.X509Enrollment + :param symmetric_key_enrollment_information: + :type symmetric_key_enrollment_information: + ~product.models.SymmetricKeyEnrollment + :param tpm_enrollment_information: + :type tpm_enrollment_information: ~product.models.TpmEnrollment + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'device_id': {'key': 'deviceId', 'type': 'str'}, + 'dps_registration_id': {'key': 'dpsRegistrationId', 'type': 'str'}, + 'region': {'key': 'region', 'type': 'str'}, + 'device_connection_string': {'key': 'deviceConnectionString', 'type': 'str'}, + 'x509_enrollment_information': {'key': 'x509EnrollmentInformation', 'type': 'X509Enrollment'}, + 'symmetric_key_enrollment_information': {'key': 'symmetricKeyEnrollmentInformation', 'type': 'SymmetricKeyEnrollment'}, + 'tpm_enrollment_information': {'key': 'tpmEnrollmentInformation', 'type': 'TpmEnrollment'}, + } + + def __init__(self, *, type=None, device_id: str=None, dps_registration_id: str=None, region=None, device_connection_string: str=None, x509_enrollment_information=None, symmetric_key_enrollment_information=None, tpm_enrollment_information=None, **kwargs) -> None: + super(ProvisioningConfiguration, self).__init__(**kwargs) + self.type = type + self.device_id = device_id + self.dps_registration_id = dps_registration_id + self.region = region + self.device_connection_string = device_connection_string + self.x509_enrollment_information = x509_enrollment_information + self.symmetric_key_enrollment_information = symmetric_key_enrollment_information + self.tpm_enrollment_information = tpm_enrollment_information diff --git a/azext_iot/sdk/product/models/schema_field.py b/azext_iot/sdk/product/models/schema_field.py new file mode 100644 index 000000000..efc98f092 --- /dev/null +++ b/azext_iot/sdk/product/models/schema_field.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SchemaField(Model): + """SchemaField. + + :param schema: + :type schema: object + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'schema': {'key': 'schema', 'type': 'object'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(SchemaField, self).__init__(**kwargs) + self.schema = kwargs.get('schema', None) + self.name = kwargs.get('name', None) + self.comment = kwargs.get('comment', None) + self.display_name = kwargs.get('display_name', None) + self.id = kwargs.get('id', None) + self.description = kwargs.get('description', None) + self.language_version = kwargs.get('language_version', None) diff --git a/azext_iot/sdk/product/models/schema_field_py3.py b/azext_iot/sdk/product/models/schema_field_py3.py new file mode 100644 index 000000000..cf559baa0 --- /dev/null +++ b/azext_iot/sdk/product/models/schema_field_py3.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SchemaField(Model): + """SchemaField. + + :param schema: + :type schema: object + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'schema': {'key': 'schema', 'type': 'object'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, *, schema=None, name: str=None, comment: str=None, display_name=None, id: str=None, description=None, language_version: int=None, **kwargs) -> None: + super(SchemaField, self).__init__(**kwargs) + self.schema = schema + self.name = name + self.comment = comment + self.display_name = display_name + self.id = id + self.description = description + self.language_version = language_version diff --git a/azext_iot/sdk/product/models/symmetric_key_enrollment.py b/azext_iot/sdk/product/models/symmetric_key_enrollment.py new file mode 100644 index 000000000..31cd3ba78 --- /dev/null +++ b/azext_iot/sdk/product/models/symmetric_key_enrollment.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SymmetricKeyEnrollment(Model): + """SymmetricKeyEnrollment. + + :param registration_id: + :type registration_id: str + :param primary_key: + :type primary_key: str + :param secondary_key: + :type secondary_key: str + :param scope_id: + :type scope_id: str + """ + + _attribute_map = { + 'registration_id': {'key': 'registrationId', 'type': 'str'}, + 'primary_key': {'key': 'primaryKey', 'type': 'str'}, + 'secondary_key': {'key': 'secondaryKey', 'type': 'str'}, + 'scope_id': {'key': 'scopeId', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(SymmetricKeyEnrollment, self).__init__(**kwargs) + self.registration_id = kwargs.get('registration_id', None) + self.primary_key = kwargs.get('primary_key', None) + self.secondary_key = kwargs.get('secondary_key', None) + self.scope_id = kwargs.get('scope_id', None) diff --git a/azext_iot/sdk/product/models/symmetric_key_enrollment_py3.py b/azext_iot/sdk/product/models/symmetric_key_enrollment_py3.py new file mode 100644 index 000000000..599c6c203 --- /dev/null +++ b/azext_iot/sdk/product/models/symmetric_key_enrollment_py3.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SymmetricKeyEnrollment(Model): + """SymmetricKeyEnrollment. + + :param registration_id: + :type registration_id: str + :param primary_key: + :type primary_key: str + :param secondary_key: + :type secondary_key: str + :param scope_id: + :type scope_id: str + """ + + _attribute_map = { + 'registration_id': {'key': 'registrationId', 'type': 'str'}, + 'primary_key': {'key': 'primaryKey', 'type': 'str'}, + 'secondary_key': {'key': 'secondaryKey', 'type': 'str'}, + 'scope_id': {'key': 'scopeId', 'type': 'str'}, + } + + def __init__(self, *, registration_id: str=None, primary_key: str=None, secondary_key: str=None, scope_id: str=None, **kwargs) -> None: + super(SymmetricKeyEnrollment, self).__init__(**kwargs) + self.registration_id = registration_id + self.primary_key = primary_key + self.secondary_key = secondary_key + self.scope_id = scope_id diff --git a/azext_iot/sdk/product/models/system_error.py b/azext_iot/sdk/product/models/system_error.py new file mode 100644 index 000000000..774f0ba92 --- /dev/null +++ b/azext_iot/sdk/product/models/system_error.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SystemError(Model): + """SystemError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(SystemError, self).__init__(**kwargs) + self.message = kwargs.get('message', None) + self.code = kwargs.get('code', None) + self.details = kwargs.get('details', None) diff --git a/azext_iot/sdk/product/models/system_error_py3.py b/azext_iot/sdk/product/models/system_error_py3.py new file mode 100644 index 000000000..7b9a097ba --- /dev/null +++ b/azext_iot/sdk/product/models/system_error_py3.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SystemError(Model): + """SystemError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, *, message: str=None, code: int=None, details=None, **kwargs) -> None: + super(SystemError, self).__init__(**kwargs) + self.message = message + self.code = code + self.details = details diff --git a/azext_iot/sdk/product/models/telemetry_model.py b/azext_iot/sdk/product/models/telemetry_model.py new file mode 100644 index 000000000..d16e5e977 --- /dev/null +++ b/azext_iot/sdk/product/models/telemetry_model.py @@ -0,0 +1,60 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TelemetryModel(Model): + """TelemetryModel. + + :param supplemental_types: + :type supplemental_types: list[str] + :param schema: + :type schema: object + :param supplemental_properties: + :type supplemental_properties: dict[str, object] + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'supplemental_types': {'key': 'supplementalTypes', 'type': '[str]'}, + 'schema': {'key': 'schema', 'type': 'object'}, + 'supplemental_properties': {'key': 'supplementalProperties', 'type': '{object}'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(TelemetryModel, self).__init__(**kwargs) + self.supplemental_types = kwargs.get('supplemental_types', None) + self.schema = kwargs.get('schema', None) + self.supplemental_properties = kwargs.get('supplemental_properties', None) + self.name = kwargs.get('name', None) + self.comment = kwargs.get('comment', None) + self.display_name = kwargs.get('display_name', None) + self.id = kwargs.get('id', None) + self.description = kwargs.get('description', None) + self.language_version = kwargs.get('language_version', None) diff --git a/azext_iot/sdk/product/models/telemetry_model_py3.py b/azext_iot/sdk/product/models/telemetry_model_py3.py new file mode 100644 index 000000000..208f9d7d6 --- /dev/null +++ b/azext_iot/sdk/product/models/telemetry_model_py3.py @@ -0,0 +1,60 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TelemetryModel(Model): + """TelemetryModel. + + :param supplemental_types: + :type supplemental_types: list[str] + :param schema: + :type schema: object + :param supplemental_properties: + :type supplemental_properties: dict[str, object] + :param name: + :type name: str + :param comment: + :type comment: str + :param display_name: + :type display_name: dict[str, str] + :param id: + :type id: str + :param description: + :type description: dict[str, str] + :param language_version: + :type language_version: int + """ + + _attribute_map = { + 'supplemental_types': {'key': 'supplementalTypes', 'type': '[str]'}, + 'schema': {'key': 'schema', 'type': 'object'}, + 'supplemental_properties': {'key': 'supplementalProperties', 'type': '{object}'}, + 'name': {'key': 'name', 'type': 'str'}, + 'comment': {'key': 'comment', 'type': 'str'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'id': {'key': 'id', 'type': 'str'}, + 'description': {'key': 'description', 'type': '{str}'}, + 'language_version': {'key': 'languageVersion', 'type': 'int'}, + } + + def __init__(self, *, supplemental_types=None, schema=None, supplemental_properties=None, name: str=None, comment: str=None, display_name=None, id: str=None, description=None, language_version: int=None, **kwargs) -> None: + super(TelemetryModel, self).__init__(**kwargs) + self.supplemental_types = supplemental_types + self.schema = schema + self.supplemental_properties = supplemental_properties + self.name = name + self.comment = comment + self.display_name = display_name + self.id = id + self.description = description + self.language_version = language_version diff --git a/azext_iot/sdk/product/models/telemetry_test.py b/azext_iot/sdk/product/models/telemetry_test.py new file mode 100644 index 000000000..1ebb62942 --- /dev/null +++ b/azext_iot/sdk/product/models/telemetry_test.py @@ -0,0 +1,48 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TelemetryTest(Model): + """TelemetryTest. + + :param telemetry: + :type telemetry: ~product.models.TelemetryModel + :param expected_result: + :type expected_result: str + :param message_count: + :type message_count: int + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'telemetry': {'key': 'telemetry', 'type': 'TelemetryModel'}, + 'expected_result': {'key': 'expectedResult', 'type': 'str'}, + 'message_count': {'key': 'messageCount', 'type': 'int'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(TelemetryTest, self).__init__(**kwargs) + self.telemetry = kwargs.get('telemetry', None) + self.expected_result = kwargs.get('expected_result', None) + self.message_count = kwargs.get('message_count', None) + self.validation_timeout = kwargs.get('validation_timeout', None) + self.is_mandatory = kwargs.get('is_mandatory', None) + self.should_validate = kwargs.get('should_validate', None) diff --git a/azext_iot/sdk/product/models/telemetry_test_py3.py b/azext_iot/sdk/product/models/telemetry_test_py3.py new file mode 100644 index 000000000..53c611bd3 --- /dev/null +++ b/azext_iot/sdk/product/models/telemetry_test_py3.py @@ -0,0 +1,48 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TelemetryTest(Model): + """TelemetryTest. + + :param telemetry: + :type telemetry: ~product.models.TelemetryModel + :param expected_result: + :type expected_result: str + :param message_count: + :type message_count: int + :param validation_timeout: + :type validation_timeout: int + :param is_mandatory: + :type is_mandatory: bool + :param should_validate: + :type should_validate: bool + """ + + _attribute_map = { + 'telemetry': {'key': 'telemetry', 'type': 'TelemetryModel'}, + 'expected_result': {'key': 'expectedResult', 'type': 'str'}, + 'message_count': {'key': 'messageCount', 'type': 'int'}, + 'validation_timeout': {'key': 'validationTimeout', 'type': 'int'}, + 'is_mandatory': {'key': 'isMandatory', 'type': 'bool'}, + 'should_validate': {'key': 'shouldValidate', 'type': 'bool'}, + } + + def __init__(self, *, telemetry=None, expected_result: str=None, message_count: int=None, validation_timeout: int=None, is_mandatory: bool=None, should_validate: bool=None, **kwargs) -> None: + super(TelemetryTest, self).__init__(**kwargs) + self.telemetry = telemetry + self.expected_result = expected_result + self.message_count = message_count + self.validation_timeout = validation_timeout + self.is_mandatory = is_mandatory + self.should_validate = should_validate diff --git a/azext_iot/sdk/product/models/test_cases.py b/azext_iot/sdk/product/models/test_cases.py new file mode 100644 index 000000000..682e14dcb --- /dev/null +++ b/azext_iot/sdk/product/models/test_cases.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TestCases(Model): + """Test cases of a DigitTwin test. + + :param certification_badge_test_cases: + :type certification_badge_test_cases: list[object] + """ + + _attribute_map = { + 'certification_badge_test_cases': {'key': 'certificationBadgeTestCases', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(TestCases, self).__init__(**kwargs) + self.certification_badge_test_cases = kwargs.get('certification_badge_test_cases', None) diff --git a/azext_iot/sdk/product/models/test_cases_not_exist_error.py b/azext_iot/sdk/product/models/test_cases_not_exist_error.py new file mode 100644 index 000000000..67ae42a9a --- /dev/null +++ b/azext_iot/sdk/product/models/test_cases_not_exist_error.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TestCasesNotExistError(Model): + """TestCasesNotExistError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(TestCasesNotExistError, self).__init__(**kwargs) + self.message = kwargs.get('message', None) + self.code = kwargs.get('code', None) + self.details = kwargs.get('details', None) diff --git a/azext_iot/sdk/product/models/test_cases_not_exist_error_py3.py b/azext_iot/sdk/product/models/test_cases_not_exist_error_py3.py new file mode 100644 index 000000000..f032f60a2 --- /dev/null +++ b/azext_iot/sdk/product/models/test_cases_not_exist_error_py3.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TestCasesNotExistError(Model): + """TestCasesNotExistError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, *, message: str=None, code: int=None, details=None, **kwargs) -> None: + super(TestCasesNotExistError, self).__init__(**kwargs) + self.message = message + self.code = code + self.details = details diff --git a/azext_iot/sdk/product/models/test_cases_py3.py b/azext_iot/sdk/product/models/test_cases_py3.py new file mode 100644 index 000000000..465cc189d --- /dev/null +++ b/azext_iot/sdk/product/models/test_cases_py3.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TestCases(Model): + """Test cases of a DigitTwin test. + + :param certification_badge_test_cases: + :type certification_badge_test_cases: list[object] + """ + + _attribute_map = { + 'certification_badge_test_cases': {'key': 'certificationBadgeTestCases', 'type': '[object]'}, + } + + def __init__(self, *, certification_badge_test_cases=None, **kwargs) -> None: + super(TestCases, self).__init__(**kwargs) + self.certification_badge_test_cases = certification_badge_test_cases diff --git a/azext_iot/sdk/product/models/test_run.py b/azext_iot/sdk/product/models/test_run.py new file mode 100644 index 000000000..052f54b8a --- /dev/null +++ b/azext_iot/sdk/product/models/test_run.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TestRun(Model): + """Test run result for API. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param id: + :type id: str + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Queued', 'Started', 'Running', + 'Failed', 'Completed', 'Cancelled' + :type status: str or ~product.models.enum + :ivar certification_badge_results: + :vartype certification_badge_results: list[object] + """ + + _validation = { + 'certification_badge_results': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'certification_badge_results': {'key': 'certificationBadgeResults', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(TestRun, self).__init__(**kwargs) + self.id = kwargs.get('id', None) + self.start_time = kwargs.get('start_time', None) + self.end_time = kwargs.get('end_time', None) + self.status = kwargs.get('status', None) + self.certification_badge_results = None diff --git a/azext_iot/sdk/product/models/test_run_not_exist_error.py b/azext_iot/sdk/product/models/test_run_not_exist_error.py new file mode 100644 index 000000000..d8412548d --- /dev/null +++ b/azext_iot/sdk/product/models/test_run_not_exist_error.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TestRunNotExistError(Model): + """TestRunNotExistError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, **kwargs): + super(TestRunNotExistError, self).__init__(**kwargs) + self.message = kwargs.get('message', None) + self.code = kwargs.get('code', None) + self.details = kwargs.get('details', None) diff --git a/azext_iot/sdk/product/models/test_run_not_exist_error_py3.py b/azext_iot/sdk/product/models/test_run_not_exist_error_py3.py new file mode 100644 index 000000000..476ba5421 --- /dev/null +++ b/azext_iot/sdk/product/models/test_run_not_exist_error_py3.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TestRunNotExistError(Model): + """TestRunNotExistError. + + :param message: + :type message: str + :param code: + :type code: int + :param details: + :type details: list[object] + """ + + _attribute_map = { + 'message': {'key': 'message', 'type': 'str'}, + 'code': {'key': 'code', 'type': 'int'}, + 'details': {'key': 'details', 'type': '[object]'}, + } + + def __init__(self, *, message: str=None, code: int=None, details=None, **kwargs) -> None: + super(TestRunNotExistError, self).__init__(**kwargs) + self.message = message + self.code = code + self.details = details diff --git a/azext_iot/sdk/product/models/test_run_py3.py b/azext_iot/sdk/product/models/test_run_py3.py new file mode 100644 index 000000000..f1c71888a --- /dev/null +++ b/azext_iot/sdk/product/models/test_run_py3.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TestRun(Model): + """Test run result for API. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param id: + :type id: str + :param start_time: + :type start_time: datetime + :param end_time: + :type end_time: datetime + :param status: Possible values include: 'Queued', 'Started', 'Running', + 'Failed', 'Completed', 'Cancelled' + :type status: str or ~product.models.enum + :ivar certification_badge_results: + :vartype certification_badge_results: list[object] + """ + + _validation = { + 'certification_badge_results': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, + 'end_time': {'key': 'endTime', 'type': 'iso-8601'}, + 'status': {'key': 'status', 'type': 'str'}, + 'certification_badge_results': {'key': 'certificationBadgeResults', 'type': '[object]'}, + } + + def __init__(self, *, id: str=None, start_time=None, end_time=None, status=None, **kwargs) -> None: + super(TestRun, self).__init__(**kwargs) + self.id = id + self.start_time = start_time + self.end_time = end_time + self.status = status + self.certification_badge_results = None diff --git a/azext_iot/sdk/product/models/tpm_enrollment.py b/azext_iot/sdk/product/models/tpm_enrollment.py new file mode 100644 index 000000000..6380f643b --- /dev/null +++ b/azext_iot/sdk/product/models/tpm_enrollment.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TpmEnrollment(Model): + """TpmEnrollment. + + :param scope_id: + :type scope_id: str + :param registration_id: + :type registration_id: str + :param endorsement_key: + :type endorsement_key: str + :param storage_root_key: + :type storage_root_key: str + """ + + _attribute_map = { + 'scope_id': {'key': 'scopeId', 'type': 'str'}, + 'registration_id': {'key': 'registrationId', 'type': 'str'}, + 'endorsement_key': {'key': 'endorsementKey', 'type': 'str'}, + 'storage_root_key': {'key': 'storageRootKey', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(TpmEnrollment, self).__init__(**kwargs) + self.scope_id = kwargs.get('scope_id', None) + self.registration_id = kwargs.get('registration_id', None) + self.endorsement_key = kwargs.get('endorsement_key', None) + self.storage_root_key = kwargs.get('storage_root_key', None) diff --git a/azext_iot/sdk/product/models/tpm_enrollment_py3.py b/azext_iot/sdk/product/models/tpm_enrollment_py3.py new file mode 100644 index 000000000..97ab836ef --- /dev/null +++ b/azext_iot/sdk/product/models/tpm_enrollment_py3.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TpmEnrollment(Model): + """TpmEnrollment. + + :param scope_id: + :type scope_id: str + :param registration_id: + :type registration_id: str + :param endorsement_key: + :type endorsement_key: str + :param storage_root_key: + :type storage_root_key: str + """ + + _attribute_map = { + 'scope_id': {'key': 'scopeId', 'type': 'str'}, + 'registration_id': {'key': 'registrationId', 'type': 'str'}, + 'endorsement_key': {'key': 'endorsementKey', 'type': 'str'}, + 'storage_root_key': {'key': 'storageRootKey', 'type': 'str'}, + } + + def __init__(self, *, scope_id: str=None, registration_id: str=None, endorsement_key: str=None, storage_root_key: str=None, **kwargs) -> None: + super(TpmEnrollment, self).__init__(**kwargs) + self.scope_id = scope_id + self.registration_id = registration_id + self.endorsement_key = endorsement_key + self.storage_root_key = storage_root_key diff --git a/azext_iot/sdk/product/models/validation_problem_details.py b/azext_iot/sdk/product/models/validation_problem_details.py new file mode 100644 index 000000000..b31ec2d2c --- /dev/null +++ b/azext_iot/sdk/product/models/validation_problem_details.py @@ -0,0 +1,60 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ValidationProblemDetails(Model): + """ValidationProblemDetails. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param additional_properties: Unmatched properties from the message are + deserialized this collection + :type additional_properties: dict[str, object] + :ivar errors: + :vartype errors: dict[str, list[str]] + :param type: + :type type: str + :param title: + :type title: str + :param status: + :type status: int + :param detail: + :type detail: str + :param instance: + :type instance: str + """ + + _validation = { + 'errors': {'readonly': True}, + } + + _attribute_map = { + 'additional_properties': {'key': '', 'type': '{object}'}, + 'errors': {'key': 'errors', 'type': '{[str]}'}, + 'type': {'key': 'type', 'type': 'str'}, + 'title': {'key': 'title', 'type': 'str'}, + 'status': {'key': 'status', 'type': 'int'}, + 'detail': {'key': 'detail', 'type': 'str'}, + 'instance': {'key': 'instance', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ValidationProblemDetails, self).__init__(**kwargs) + self.additional_properties = kwargs.get('additional_properties', None) + self.errors = None + self.type = kwargs.get('type', None) + self.title = kwargs.get('title', None) + self.status = kwargs.get('status', None) + self.detail = kwargs.get('detail', None) + self.instance = kwargs.get('instance', None) diff --git a/azext_iot/sdk/product/models/validation_problem_details_py3.py b/azext_iot/sdk/product/models/validation_problem_details_py3.py new file mode 100644 index 000000000..395e3ae14 --- /dev/null +++ b/azext_iot/sdk/product/models/validation_problem_details_py3.py @@ -0,0 +1,60 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ValidationProblemDetails(Model): + """ValidationProblemDetails. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param additional_properties: Unmatched properties from the message are + deserialized this collection + :type additional_properties: dict[str, object] + :ivar errors: + :vartype errors: dict[str, list[str]] + :param type: + :type type: str + :param title: + :type title: str + :param status: + :type status: int + :param detail: + :type detail: str + :param instance: + :type instance: str + """ + + _validation = { + 'errors': {'readonly': True}, + } + + _attribute_map = { + 'additional_properties': {'key': '', 'type': '{object}'}, + 'errors': {'key': 'errors', 'type': '{[str]}'}, + 'type': {'key': 'type', 'type': 'str'}, + 'title': {'key': 'title', 'type': 'str'}, + 'status': {'key': 'status', 'type': 'int'}, + 'detail': {'key': 'detail', 'type': 'str'}, + 'instance': {'key': 'instance', 'type': 'str'}, + } + + def __init__(self, *, additional_properties=None, type: str=None, title: str=None, status: int=None, detail: str=None, instance: str=None, **kwargs) -> None: + super(ValidationProblemDetails, self).__init__(**kwargs) + self.additional_properties = additional_properties + self.errors = None + self.type = type + self.title = title + self.status = status + self.detail = detail + self.instance = instance diff --git a/azext_iot/sdk/product/models/x509_enrollment.py b/azext_iot/sdk/product/models/x509_enrollment.py new file mode 100644 index 000000000..f3f400c72 --- /dev/null +++ b/azext_iot/sdk/product/models/x509_enrollment.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class X509Enrollment(Model): + """X509Enrollment. + + :param scope_id: + :type scope_id: str + :param subject: + :type subject: str + :param thumbprint: + :type thumbprint: str + :param registration_id: + :type registration_id: str + :param base64_encoded_x509_certificate: + :type base64_encoded_x509_certificate: str + """ + + _attribute_map = { + 'scope_id': {'key': 'scopeId', 'type': 'str'}, + 'subject': {'key': 'subject', 'type': 'str'}, + 'thumbprint': {'key': 'thumbprint', 'type': 'str'}, + 'registration_id': {'key': 'registrationId', 'type': 'str'}, + 'base64_encoded_x509_certificate': {'key': 'base64EncodedX509Certificate', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(X509Enrollment, self).__init__(**kwargs) + self.scope_id = kwargs.get('scope_id', None) + self.subject = kwargs.get('subject', None) + self.thumbprint = kwargs.get('thumbprint', None) + self.registration_id = kwargs.get('registration_id', None) + self.base64_encoded_x509_certificate = kwargs.get('base64_encoded_x509_certificate', None) diff --git a/azext_iot/sdk/product/models/x509_enrollment_py3.py b/azext_iot/sdk/product/models/x509_enrollment_py3.py new file mode 100644 index 000000000..e2ac30081 --- /dev/null +++ b/azext_iot/sdk/product/models/x509_enrollment_py3.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class X509Enrollment(Model): + """X509Enrollment. + + :param scope_id: + :type scope_id: str + :param subject: + :type subject: str + :param thumbprint: + :type thumbprint: str + :param registration_id: + :type registration_id: str + :param base64_encoded_x509_certificate: + :type base64_encoded_x509_certificate: str + """ + + _attribute_map = { + 'scope_id': {'key': 'scopeId', 'type': 'str'}, + 'subject': {'key': 'subject', 'type': 'str'}, + 'thumbprint': {'key': 'thumbprint', 'type': 'str'}, + 'registration_id': {'key': 'registrationId', 'type': 'str'}, + 'base64_encoded_x509_certificate': {'key': 'base64EncodedX509Certificate', 'type': 'str'}, + } + + def __init__(self, *, scope_id: str=None, subject: str=None, thumbprint: str=None, registration_id: str=None, base64_encoded_x509_certificate: str=None, **kwargs) -> None: + super(X509Enrollment, self).__init__(**kwargs) + self.scope_id = scope_id + self.subject = subject + self.thumbprint = thumbprint + self.registration_id = registration_id + self.base64_encoded_x509_certificate = base64_encoded_x509_certificate diff --git a/azext_iot/sdk/product/version.py b/azext_iot/sdk/product/version.py new file mode 100644 index 000000000..fc729cd31 --- /dev/null +++ b/azext_iot/sdk/product/version.py @@ -0,0 +1,13 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +VERSION = "2020-05-01-preview" + diff --git a/azext_iot/tests/conftest.py b/azext_iot/tests/conftest.py index 438607886..e0bf222ac 100644 --- a/azext_iot/tests/conftest.py +++ b/azext_iot/tests/conftest.py @@ -214,3 +214,12 @@ def error_callback(_): url=re.compile(any_endpoint), ) yield rsps + + +@pytest.fixture() +def fixture_mock_aics_token(mocker): + patch = mocker.patch( + "azext_iot.product.providers.auth.AICSAuthentication.generate_token" + ) + patch.return_value = "Bearer token" + return patch diff --git a/azext_iot/tests/product/__init__.py b/azext_iot/tests/product/__init__.py new file mode 100644 index 000000000..abb53840c --- /dev/null +++ b/azext_iot/tests/product/__init__.py @@ -0,0 +1,16 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.testsdk import LiveScenarioTest + + +class AICSLiveScenarioTest(LiveScenarioTest): + def __init__(self, test_scenario): + assert test_scenario + + super(AICSLiveScenarioTest, self).__init__(test_scenario) + AICSLiveScenarioTest.handle = self + self.kwargs.update({"BASE_URL": "https://test.certsvc.trafficmanager.net"}) diff --git a/azext_iot/tests/product/test_aics_e2e_int.py b/azext_iot/tests/product/test_aics_e2e_int.py new file mode 100644 index 000000000..d91a1a114 --- /dev/null +++ b/azext_iot/tests/product/test_aics_e2e_int.py @@ -0,0 +1,171 @@ +# 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 time import sleep +from . import AICSLiveScenarioTest +from azext_iot.product.shared import ( + TaskType, + BadgeType, + DeviceTestTaskStatus, + AttestationType, +) + + +class TestProductDeviceTestTasks(AICSLiveScenarioTest): + def __init__(self, test_case): + super(TestProductDeviceTestTasks, self).__init__(test_case) + self.kwargs.update( + { + "generate_task": TaskType.GenerateTestCases.value, + "queue_task": TaskType.QueueTestRun.value, + } + ) + + def test_e2e(self): + + # Test requirement list + self.cmd("iot product requirement list --base-url {BASE_URL}") + + self.kwargs.update({"badge_type": BadgeType.IotDevice.value}) + requirements_output = self.cmd( + "iot product requirement list --bt {badge_type} --base-url {BASE_URL}" + ).get_output_in_json() + expected = [ + { + "badgeType": "IotDevice", + "provisioningRequirement": { + "provisioningTypes": ["SymmetricKey", "TPM", "X509"] + }, + } + ] + + assert requirements_output == expected + # Device test operations + test = self.cmd( + "iot product test create --at SymmetricKey --dt DevKit --base-url {BASE_URL}" + ).get_output_in_json() + assert test["deviceType"].lower() == "devkit" + assert test["provisioningConfiguration"]["type"].lower() == "symmetrickey" + assert test["provisioningConfiguration"]["symmetricKeyEnrollmentInformation"][ + "primaryKey" + ] + + self.kwargs.update({"device_test_id": test["id"]}) + + test = self.cmd( + "iot product test show -t {device_test_id} --base-url {BASE_URL}" + ).get_output_in_json() + assert test["id"] == self.kwargs["device_test_id"] + + updated = self.cmd( + "iot product test update -t {device_test_id} --at symmetricKey --base-url {BASE_URL}" + ).get_output_in_json() + assert updated["id"] == self.kwargs["device_test_id"] + assert ( + updated["provisioningConfiguration"]["type"] + == AttestationType.symmetricKey.value + ) + assert updated["provisioningConfiguration"]["symmetricKeyEnrollmentInformation"] + + # Generate test cases + generate_task = self.cmd( + "iot product test task create -t {device_test_id} --type {generate_task} --base-url {BASE_URL}" + ).get_output_in_json() + assert generate_task["status"] == DeviceTestTaskStatus.queued.value + + test_task = self.cmd( + "iot product test task show --running -t {device_test_id} --base-url {BASE_URL}" + ).get_output_in_json()[0] + + assert json.dumps(test_task) + assert test_task.get("status") == DeviceTestTaskStatus.queued.value + assert test_task.get("error") is None + assert test_task.get("type") == TaskType.GenerateTestCases.value + + # wait for generate task to complete + sleep(5) + + self.kwargs.update({"generate_task_id": test_task["id"]}) + test_task = self.cmd( + "iot product test task show -t {device_test_id} --task-id {generate_task_id} --base-url {BASE_URL}" + ).get_output_in_json() + assert test_task.get("status") == DeviceTestTaskStatus.completed.value + assert test_task.get("error") is None + assert test_task.get("type") == TaskType.GenerateTestCases.value + + # Test case operations + case_list = self.cmd( + "iot product test case list -t {device_test_id} --base-url {BASE_URL}" + ).get_output_in_json() + assert json.dumps(case_list) + assert json.dumps(case_list["certificationBadgeTestCases"]) + + # TODO: Test case update + + # Queue a test run, await the run results + run = self.cmd( + "iot product test task create -t {device_test_id} --type {queue_task} --wait --base-url {BASE_URL}" + ).get_output_in_json() + # test run currently fails without simulator + assert run["status"] == DeviceTestTaskStatus.failed.value + assert json.dumps(run["certificationBadgeResults"]) + + self.kwargs.update({"run_id": run["id"]}) + # show run + run_get = self.cmd( + "iot product test run show -t {device_test_id} -r {run_id} --base-url {BASE_URL}" + ).get_output_in_json() + # show latest run + run_latest = self.cmd( + "iot product test run show -t {device_test_id} --base-url {BASE_URL}" + ).get_output_in_json() + assert run_get == run_latest + assert run_get["id"] == run_latest["id"] == self.kwargs["run_id"] + assert ( + run_get["status"] + == run_latest["status"] + == DeviceTestTaskStatus.failed.value + ) + + # Queue a test run without wait, get run_id + queue_task = self.cmd( + "iot product test task create -t {device_test_id} --type {queue_task} --base-url {BASE_URL}" + ).get_output_in_json() + assert queue_task["type"] == TaskType.QueueTestRun.value + assert queue_task["status"] == DeviceTestTaskStatus.queued.value + + self.kwargs.update({"queue_task_id": queue_task["id"]}) + + # allow test to start running + sleep(5) + + queue_task = self.cmd( + "iot product test task show -t {device_test_id} --task-id {queue_task_id} --base-url {BASE_URL}" + ).get_output_in_json() + assert queue_task["type"] == TaskType.QueueTestRun.value + assert queue_task["status"] == DeviceTestTaskStatus.running.value + + # Cancel running test task + self.cmd( + "iot product test task delete -t {device_test_id} --task-id {queue_task_id} --base-url {BASE_URL}" + ) + + # allow test to be cancelled + sleep(5) + + # get cancelled test task + show = self.cmd( + "iot product test task show -t {device_test_id} --task-id {queue_task_id} --base-url {BASE_URL}" + ).get_output_in_json() + + assert show["status"] == DeviceTestTaskStatus.cancelled.value + + # # Submit run + # self.cmd( + # "iot product test run submit -t {device_test_id} -r {run_id} --base-url {BASE_URL}", + # expect_failure=True, + # ) diff --git a/azext_iot/tests/product/test_command_requirement_list_int.py b/azext_iot/tests/product/test_command_requirement_list_int.py new file mode 100644 index 000000000..f78d76a6a --- /dev/null +++ b/azext_iot/tests/product/test_command_requirement_list_int.py @@ -0,0 +1,63 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from . import AICSLiveScenarioTest + + +class TestRequirementList(AICSLiveScenarioTest): + def test_list_default(self): + create_output = self.cmd( + "iot product requirement list --base-url {BASE_URL}" + ).get_output_in_json() + expected = [ + { + "badgeType": "IotDevice", + "provisioningRequirement": { + "provisioningTypes": ["SymmetricKey", "TPM", "X509"] + }, + } + ] + assert create_output == expected + + def test_list_device(self): + create_output = self.cmd( + "iot product requirement list --badge-type IotDevice --base-url {BASE_URL}" + ).get_output_in_json() + expected = [ + { + "badgeType": "IotDevice", + "provisioningRequirement": { + "provisioningTypes": ["SymmetricKey", "TPM", "X509"] + }, + } + ] + assert create_output == expected + + def test_list_edge(self): + create_output = self.cmd( + "iot product requirement list --badge-type IotEdgeCompatible --base-url {BASE_URL}" + ).get_output_in_json() + expected = [ + { + "badgeType": "IotEdgeCompatible", + "provisioningRequirement": {"provisioningTypes": ["ConnectionString"]}, + } + ] + assert create_output == expected + + def test_list_pnp(self): + create_output = self.cmd( + "iot product requirement list --badge-type Pnp --base-url {BASE_URL}" + ).get_output_in_json() + expected = [ + { + "badgeType": "Pnp", + "provisioningRequirement": { + "provisioningTypes": ["SymmetricKey", "TPM", "X509"] + }, + } + ] + assert create_output == expected diff --git a/azext_iot/tests/product/test_command_test_case_list_int.py b/azext_iot/tests/product/test_command_test_case_list_int.py new file mode 100644 index 000000000..bae26b222 --- /dev/null +++ b/azext_iot/tests/product/test_command_test_case_list_int.py @@ -0,0 +1,27 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from . import AICSLiveScenarioTest +from azext_iot.product.shared import BadgeType + + +class TestTestShowInt(AICSLiveScenarioTest): + def __init__(self, test_case): + self.test_id = "524ac74f-752b-4748-9667-45cd09e8a098" + super(TestTestShowInt, self).__init__(test_case) + + def test_case_list_test(self): + # call the GET /deviceTest/{test_id} + output = self.cmd( + "iot product test case list -t {} --base-url {}".format( + self.test_id, self.kwargs["BASE_URL"] + ) + ).get_output_in_json() + + assert ( + output["certificationBadgeTestCases"][0]["type"].lower() + == BadgeType.Pnp.value.lower() + ) diff --git a/azext_iot/tests/product/test_command_test_case_update_unit.py b/azext_iot/tests/product/test_command_test_case_update_unit.py new file mode 100644 index 000000000..e2573a344 --- /dev/null +++ b/azext_iot/tests/product/test_command_test_case_update_unit.py @@ -0,0 +1,53 @@ +# 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 unittest +import mock +from knack.util import CLIError +from azext_iot.product.test.command_test_cases import update + + +class TestTestCaseUpdate(unittest.TestCase): + def __init__(self, test_case): + self.test_id = "3beb0e67-33d0-4896-b69b-91c7b7ce8fab" + super(TestTestCaseUpdate, self).__init__(test_case) + + @mock.patch("os.path.exists") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.update_test_cases") + def test_update_with_missing_file(self, mock_api, mock_exists): + mock_exists.return_value = False + + with self.assertRaises(CLIError) as context: + update( + self, + test_id=self.test_id, + configuration_file="missingFile.json" + ) + + self.assertEqual( + "If attestation type is x509, certificate path is required", + str(context.exception), + ) + mock_api.assert_not_called() + + @mock.patch("os.path.exists") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.update_test_cases") + @mock.patch("azext_iot.product.test.command_test_cases.process_json_arg") + def test_update(self, mock_json_parser, mock_api, mock_exists): + mock_exists.return_value = True + mock_json_payload = {} + mock_json_parser.return_value = mock_json_payload + + update( + self, + test_id=self.test_id, + configuration_file="configurationFile.json" + ) + + mock_api.assert_called_with( + device_test_id=self.test_id, + certification_badge_test_cases=mock_json_payload + ) diff --git a/azext_iot/tests/product/test_command_test_create_int.py b/azext_iot/tests/product/test_command_test_create_int.py new file mode 100644 index 000000000..2991194ee --- /dev/null +++ b/azext_iot/tests/product/test_command_test_create_int.py @@ -0,0 +1,38 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from . import AICSLiveScenarioTest +from azext_iot.product.shared import AttestationType, BadgeType, DeviceType + + +class TestTestCreateInt(AICSLiveScenarioTest): + def __init__(self, test_case): + super(TestTestCreateInt, self).__init__(test_case) + + def test_create_symmetric_key(self): + device_type = DeviceType.DevKit.value + attestation_type = AttestationType.symmetricKey.value + badge_type = BadgeType.IotDevice.value + + # call the POST /deviceTest + output = self.cmd( + "iot product test create --dt {} --at {} --bt {} --base-url {}".format( + device_type, + attestation_type, + badge_type, + self.kwargs["BASE_URL"], + ) + ).get_output_in_json() + + assert output["deviceType"].lower() == device_type.lower() + assert ( + output["provisioningConfiguration"]["type"].lower() + == attestation_type.lower() + ) + # assert service created symmetric key info + assert output["provisioningConfiguration"]["symmetricKeyEnrollmentInformation"][ + "primaryKey" + ] diff --git a/azext_iot/tests/product/test_command_test_create_unit.py b/azext_iot/tests/product/test_command_test_create_unit.py new file mode 100644 index 000000000..d4a557961 --- /dev/null +++ b/azext_iot/tests/product/test_command_test_create_unit.py @@ -0,0 +1,302 @@ +# 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 unittest +import mock +from knack.util import CLIError +from azext_iot.product.test.command_tests import create, _process_models_directory as process_models +from azext_iot.product.shared import BadgeType, AttestationType, DeviceType, ValidationType + + +class TestTestCreateUnit(unittest.TestCase): + def __init__(self, test_case): + super(TestTestCreateUnit, self).__init__(test_case) + + def test_create_with_no_parameters_fails(self): + with self.assertRaises(CLIError): + create(self) + + def test_create_with_x509_and_no_certificate_fails(self): + with self.assertRaises(CLIError) as context: + create(self, attestation_type=AttestationType.x509.value) + + self.assertEqual( + "If attestation type is x509, certificate path is required", + str(context.exception), + ) + + def test_create_with_tpm_and_no_endorsement_key_fails(self): + with self.assertRaises(CLIError) as context: + create(self, attestation_type=AttestationType.tpm.value) + + self.assertEqual( + "If attestation type is TPM, endorsement key is required", + str(context.exception), + ) + + def test_edge_module_without_connection_string_fails(self): + with self.assertRaises(CLIError) as context: + create( + self, + attestation_type=AttestationType.connectionString.value, + badge_type=BadgeType.IotEdgeCompatible.value, + ) + + self.assertEqual( + "Connection string is required for Edge Compatible modules testing", + str(context.exception), + ) + + def test_connection_string_for_pnp_fails(self): + with self.assertRaises(CLIError) as context: + create( + self, + attestation_type=AttestationType.connectionString.value, + badge_type=BadgeType.Pnp.value, + models="./stuff", + ) + + self.assertEqual( + "Connection string is only available for Edge Compatible modules testing", + str(context.exception), + ) + + def test_connection_string_for_iot_device_fails(self): + with self.assertRaises(CLIError) as context: + create(self, attestation_type=AttestationType.connectionString.value) + + self.assertEqual( + "Connection string is only available for Edge Compatible modules testing", + str(context.exception), + ) + + def test_create_with_pnp_and_no_models_fails(self): + with self.assertRaises(CLIError) as context: + create(self, badge_type=BadgeType.Pnp.value) + + self.assertEqual( + "If badge type is Pnp, models is required", str(context.exception) + ) + + def test_create_with_missing_device_type_fails(self): + with self.assertRaises(CLIError) as context: + create( + self, + attestation_type=AttestationType.symmetricKey.value, + badge_type=BadgeType.Pnp.value, + models="models_folder", + ) + + self.assertEqual( + "If configuration file is not specified, attestation and device definition parameters must be specified", + str(context.exception), + ) + + def test_create_certification_with_missing_product_id_fails(self): + with self.assertRaises(CLIError) as context: + create( + self, + attestation_type=AttestationType.symmetricKey.value, + device_type=DeviceType.DevKit.value, + badge_type=BadgeType.Pnp.value, + models="models_folder", + validation_type=ValidationType.certification.value + ) + self.assertEqual( + "Product Id is required for validation type Certification", + str(context.exception), + ) + + @mock.patch("azext_iot.product.test.command_tests._process_models_directory") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test") + def test_create_with_default_badge_type_doesnt_check_models( + self, mock_service, mock_process_models + ): + create( + self, + attestation_type=AttestationType.symmetricKey.value, + device_type=DeviceType.DevKit.value, + models="models_folder", + ) + + mock_process_models.assert_not_called() + mock_service.assert_called_with( + provisioning=True, + body={ + "validationType": "Test", + "productId": None, + "deviceType": "DevKit", + "provisioningConfiguration": { + "type": "SymmetricKey", + "symmetricKeyEnrollmentInformation": {}, + }, + "certificationBadgeConfigurations": [{"type": "IotDevice"}], + }, + ) + + @mock.patch("azext_iot.product.test.command_tests._process_models_directory") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test") + def test_create_with_pnp_badge_type_checks_models( + self, mock_service, mock_process_models + ): + mock_process_models.return_value = [ + '{"@id":"model1"}', + '{"@id":"model2"}', + '{"@id":"model3"}', + ] + create( + self, + attestation_type=AttestationType.symmetricKey.value, + device_type=DeviceType.DevKit.value, + models="models_folder", + badge_type=BadgeType.Pnp.value, + ) + + mock_process_models.assert_called_with("models_folder") + mock_service.assert_called_with( + provisioning=True, + body={ + "validationType": "Test", + "productId": None, + "deviceType": "DevKit", + "provisioningConfiguration": { + "type": "SymmetricKey", + "symmetricKeyEnrollmentInformation": {}, + }, + "certificationBadgeConfigurations": [ + { + "type": "Pnp", + "digitalTwinModelDefinitions": [ + '{"@id":"model1"}', + '{"@id":"model2"}', + '{"@id":"model3"}', + ], + } + ], + }, + ) + + @mock.patch("azext_iot.product.test.command_tests._read_certificate_from_file") + @mock.patch("azext_iot.product.test.command_tests._process_models_directory") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test") + def test_create_with_cert_auth_reads_cert_file( + self, mock_service, mock_process_models, mock_read_certificate + ): + mock_read_certificate.return_value = "MockBase64String" + mock_process_models.return_value = [ + '{"@id":"model1"}', + '{"@id":"model2"}', + '{"@id":"model3"}', + ] + create( + self, + attestation_type=AttestationType.x509.value, + device_type=DeviceType.DevKit.value, + models="models_folder", + badge_type=BadgeType.Pnp.value, + certificate_path="mycertificate.cer", + product_id="ABC123", + validation_type=ValidationType.certification.value + ) + + mock_read_certificate.assert_called_with("mycertificate.cer") + mock_process_models.assert_called_with("models_folder") + mock_service.assert_called_with( + provisioning=True, + body={ + "validationType": "Certification", + "productId": "ABC123", + "deviceType": "DevKit", + "provisioningConfiguration": { + "type": "X509", + "x509EnrollmentInformation": { + "base64EncodedX509Certificate": "MockBase64String" + }, + }, + "certificationBadgeConfigurations": [ + { + "type": "Pnp", + "digitalTwinModelDefinitions": [ + '{"@id":"model1"}', + '{"@id":"model2"}', + '{"@id":"model3"}', + ], + } + ], + }, + ) + + @mock.patch("azext_iot.product.test.command_tests._read_certificate_from_file") + @mock.patch("azext_iot.product.test.command_tests._process_models_directory") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test") + def test_create_with_tpm( + self, mock_service, mock_process_models, mock_read_certificate + ): + mock_process_models.return_value = [ + '{"@id":"model1"}', + '{"@id":"model2"}', + '{"@id":"model3"}', + ] + create( + self, + attestation_type=AttestationType.tpm.value, + endorsement_key="12345", + device_type=DeviceType.DevKit.value, + models="models_folder", + badge_type=BadgeType.Pnp.value, + certificate_path="mycertificate.cer", + ) + + mock_read_certificate.assert_not_called() + mock_process_models.assert_called_with("models_folder") + mock_service.assert_called_with( + provisioning=True, + body={ + "validationType": "Test", + "productId": None, + "deviceType": "DevKit", + "provisioningConfiguration": { + "type": "TPM", + "tpmEnrollmentInformation": {"endorsementKey": "12345"}, + }, + "certificationBadgeConfigurations": [ + { + "type": "Pnp", + "digitalTwinModelDefinitions": [ + '{"@id":"model1"}', + '{"@id":"model2"}', + '{"@id":"model3"}', + ], + } + ], + }, + ) + + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test") + @mock.patch("azext_iot.product.test.command_tests._create_from_file") + def test_create_with_configuration_file(self, mock_from_file, mock_sdk_create): + mock_file_data = {"mock": "data"} + mock_from_file.return_value = mock_file_data + create(self, configuration_file="somefile") + mock_from_file.assert_called_with("somefile") + mock_sdk_create.assert_called_with(provisioning=True, body=mock_file_data) + + @mock.patch("os.scandir") + @mock.patch("os.path.isfile") + @mock.patch("azext_iot.common.utility.read_file_content") + def test_process_models_directory_as_file(self, mock_file_content, mock_is_file, mock_scan_tree): + mock_file_content.return_value = {"id": "my file"} + mock_is_file.return_value = True + + results = process_models("myPath.dtdl") + + self.assertEqual(len(results), 1) + mock_scan_tree.assert_not_called() + + results = process_models("myPath.json") + + self.assertEqual(len(results), 1) + mock_scan_tree.assert_not_called() diff --git a/azext_iot/tests/product/test_command_test_run_int.py b/azext_iot/tests/product/test_command_test_run_int.py new file mode 100644 index 000000000..61d812822 --- /dev/null +++ b/azext_iot/tests/product/test_command_test_run_int.py @@ -0,0 +1,65 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from . import AICSLiveScenarioTest +from azext_iot.product.shared import TaskType + + +class TestProductDeviceTestRuns(AICSLiveScenarioTest): + def __init__(self, _): + super(TestProductDeviceTestRuns, self).__init__(_) + self.kwargs.update( + { + "device_test_id": "524ac74f-752b-4748-9667-45cd09e8a098", + "generate_task": TaskType.GenerateTestCases.value, + "run_task": TaskType.QueueTestRun.value, + } + ) + + def setup(self): + # setup test runs + gen_task_id = self.cmd( + "iot product test task create -t {device_test_id} --type {generate_task} --wait --base-url {BASE_URL}" + ).get_output_in_json()["id"] + queue_task_id = self.cmd( + "iot product test task create -t {device_test_id} --type {run_task} --wait --base-url {BASE_URL}" + ).get_output_in_json()["id"] + self.kwargs.update( + {"generate_task_id": gen_task_id, "queue_task_id": queue_task_id} + ) + + def teardown(self): + self.cmd( + "iot product test task delete -t {device_test_id} --task-id {generate_task_id} --base-url {BASE_URL}" + ) + self.cmd( + "iot product test task delete -t {device_test_id} --task-id {queue_task_id} --base-url {BASE_URL}" + ) + + def test_product_device_test_run(self): + # get latest test run + latest = self.cmd( + "iot product test run show -t {device_test_id} --base-url {BASE_URL}" + ).get_output_in_json() + run_id = latest["id"] + self.kwargs.update({"test_run_id": run_id}) + specific = self.cmd( + "iot product test run show -t {device_test_id} -r {test_run_id} --base-url {BASE_URL}" + ).get_output_in_json() + + assert latest == specific + + # bad test/run id + self.cmd( + "iot product test run show -t bad_test_id -r bad_run_id --base-url {BASE_URL}", + expect_failure=True, + ) + + # submit (currently cannot submit failed test) + self.cmd( + "iot product test run submit -t {device_test_id} -r {test_run_id} --base-url {BASE_URL}", + expect_failure=True, + ) diff --git a/azext_iot/tests/product/test_command_test_run_unit.py b/azext_iot/tests/product/test_command_test_run_unit.py new file mode 100644 index 000000000..ceb80792a --- /dev/null +++ b/azext_iot/tests/product/test_command_test_run_unit.py @@ -0,0 +1,240 @@ +# 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 unittest +import pytest +import mock +import json +import responses +from datetime import datetime +from knack.util import CLIError +from azext_iot.product.test.command_test_runs import show, submit +from azext_iot.sdk.product.models import TestRun +from azext_iot.product.shared import BASE_URL + +mock_target = {} +mock_target["entity"] = BASE_URL +device_test_id = "12345" +device_test_run_id = "67890" + +api_string = "?api-version=2020-05-01-preview" + +patch_get_latest_test_run = "azext_iot.sdk.product.aicsapi.AICSAPI.get_latest_test_run" +patch_get_test_run = "azext_iot.sdk.product.aicsapi.AICSAPI.get_test_run" +patch_submit_test_run = "azext_iot.sdk.product.aicsapi.AICSAPI.submit_test_run" + +queued_task = TestRun( + id=device_test_run_id, + start_time=datetime.now(), + end_time=datetime.now(), + status="Queued", +) +started_task = TestRun( + id=device_test_run_id, + start_time=datetime.now(), + end_time=datetime.now(), + status="Started", +) +running_task = TestRun( + id=device_test_run_id, + start_time=datetime.now(), + end_time=datetime.now(), + status="Running", +) +completed_task = TestRun( + id=device_test_run_id, + start_time=datetime.now(), + end_time=datetime.now(), + status="Completed", +) + +queued_task_obj = { + "id": queued_task.id, + "start_time": "start_time", + "end_time": "end_time", + "status": "Queued", +} + +run_result_body = json.dumps(queued_task_obj) +finished_run_result_body = run_result_body.replace("Queued", "Completed") + + +class TestRunShow(unittest.TestCase): + @mock.patch(patch_get_latest_test_run) + @mock.patch(patch_get_test_run) + def test_run_show_latest(self, mock_get, mock_get_latest): + show(self, test_id=device_test_id) + + # no run_id, so should call get_latest + mock_get_latest.assert_called_with(device_test_id=device_test_id) + mock_get.assert_not_called() + + @mock.patch(patch_get_latest_test_run) + @mock.patch(patch_get_test_run) + def test_run_show(self, mock_get, mock_get_latest): + show(self, test_id=device_test_id, run_id=device_test_run_id) + + # one call to get + mock_get.assert_called_with( + device_test_id=device_test_id, test_run_id=device_test_run_id + ) + + # does not call get_latest + mock_get_latest.assert_not_called() + + @mock.patch(patch_get_latest_test_run, return_value=queued_task) + @mock.patch( + patch_get_test_run, + side_effect=iter([started_task, running_task, completed_task]), + ) + def test_run_show_latest_wait(self, mock_get, mock_get_latest): + result = show(self, test_id=device_test_id, wait=True, poll_interval=1) + assert mock_get_latest.call_count == 1 + + # three calls to 'get' until status is 'Completed', using run-id from get_latest call + mock_get.assert_called_with( + test_run_id=mock_get_latest.return_value.id, device_test_id=device_test_id + ) + assert mock_get.call_count == 3 + + # make sure we get the last result back + assert result.status == "Completed" + + @mock.patch(patch_get_test_run, return_value=None) + def test_sdk_run_show_error(self, fixture_cmd): + with self.assertRaises(CLIError) as context: + show(fixture_cmd, test_id=device_test_id, run_id=device_test_run_id) + + self.assertEqual( + "No test run found for test ID '{}' with run ID '{}'".format( + device_test_id, device_test_run_id + ), + str(context.exception), + ) + + +class TestRunSubmit(unittest.TestCase): + @mock.patch(patch_submit_test_run) + def test_run_submit(self, mock_submit): + submit(self, test_id=device_test_id, run_id=device_test_run_id) + mock_submit.assert_called_with( + device_test_id=device_test_id, test_run_id=device_test_run_id + ) + + +class TestRunSDK(object): + + # Gets + @pytest.fixture(params=[200]) + def service_client_get(self, mocked_response, fixture_mock_aics_token, request): + # get latest + mocked_response.add( + method=responses.GET, + url="{}/deviceTests/{}/testRuns/latest{}".format( + mock_target["entity"], device_test_id, api_string + ), + body=run_result_body, + headers={"x-ms-command-statuscode": str(request.param)}, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + # get specific (completed) + mocked_response.add( + method=responses.GET, + url="{}/deviceTests/{}/testRuns/{}{}".format( + mock_target["entity"], device_test_id, device_test_run_id, api_string + ), + body=finished_run_result_body, + headers={"x-ms-command-statuscode": str(request.param)}, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response + + # submit + @pytest.fixture(params=[204]) + def service_client_submit(self, mocked_response, fixture_mock_aics_token, request): + mocked_response.add( + method=responses.POST, + url="{}/deviceTests/{}/testRuns/{}/submit{}".format( + mock_target["entity"], device_test_id, device_test_run_id, api_string + ), + body="{}", + headers={"x-ms-command-statuscode": str(request.param)}, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response + + # get error (invalid test task or run id) + @pytest.fixture(params=[404]) + def service_client_error(self, mocked_response, fixture_mock_aics_token, request): + mocked_response.add( + method=responses.GET, + url="{}/deviceTests/{}/testRuns/{}{}".format( + mock_target["entity"], device_test_id, device_test_run_id, api_string + ), + body="", + headers={"x-ms-command-statuscode": str(request.param)}, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + def test_sdk_run_show(self, fixture_cmd, service_client_get): + # get latest run + result = show(fixture_cmd, test_id=device_test_id) + req = service_client_get.calls[0].request + + assert "deviceTests/{}/testRuns/latest".format(device_test_id) in req.url + assert req.method == "GET" + assert result.id == device_test_run_id + + # specific run + result = show(fixture_cmd, test_id=device_test_id, run_id=device_test_run_id) + req = service_client_get.calls[1].request + + assert ( + "deviceTests/{}/testRuns/{}".format(device_test_id, device_test_run_id) + in req.url + ) + assert req.method == "GET" + assert result.id == device_test_run_id + + # get latest, with wait + def test_sdk_run_show_wait(self, fixture_cmd, service_client_get): + result = show(fixture_cmd, test_id=device_test_id, wait=True, poll_interval=1) + reqs = list(map(lambda call: call.request, service_client_get.calls)) + assert reqs[0].method == "GET" + url = reqs[0].url + assert "deviceTests/{}/testRuns/latest".format(device_test_id) in url + + assert reqs[1].method == "GET" + url = reqs[1].url + assert ( + "deviceTests/{}/testRuns/{}".format(device_test_id, device_test_run_id) + in url + ) + + assert result.id == device_test_run_id + assert result.status == "Completed" + + def test_sdk_task_submit(self, fixture_cmd, service_client_submit): + result = submit(fixture_cmd, test_id=device_test_id, run_id=device_test_run_id) + assert not result + + req = service_client_submit.calls[0].request + url = req.url + assert req.method == "POST" + assert ( + "deviceTests/{}/testRuns/{}/submit".format( + device_test_id, device_test_run_id + ) + in url + ) diff --git a/azext_iot/tests/product/test_command_test_search_unit.py b/azext_iot/tests/product/test_command_test_search_unit.py new file mode 100644 index 000000000..9eac7c744 --- /dev/null +++ b/azext_iot/tests/product/test_command_test_search_unit.py @@ -0,0 +1,51 @@ +# 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 unittest +import mock +from knack.util import CLIError +from azext_iot.product.test.command_tests import search +from azext_iot.sdk.product.models import DeviceTestSearchOptions + + +class SearchClass(unittest.TestCase): + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.search_device_test") + def test_search_not_called_when_no_criteria(self, mock_search): + with self.assertRaises(CLIError): + search(self) + + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.search_device_test") + def test_search_called_with_product_id(self, mock_search): + search(self, product_id="123") + mock_search.assert_called_with( + body=DeviceTestSearchOptions( + product_id="123", + dps_registration_id=None, + dps_x509_certificate_common_name=None, + ) + ) + + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.search_device_test") + def test_search_called_with_registration_id(self, mock_search): + search(self, registration_id="123") + mock_search.assert_called_with( + body=DeviceTestSearchOptions( + product_id=None, + dps_registration_id="123", + dps_x509_certificate_common_name=None, + ) + ) + + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.search_device_test") + def test_search_called_with_certificate_name(self, mock_search): + search(self, certificate_name="123") + mock_search.assert_called_with( + body=DeviceTestSearchOptions( + product_id=None, + dps_registration_id=None, + dps_x509_certificate_common_name="123", + ) + ) diff --git a/azext_iot/tests/product/test_command_test_show_int.py b/azext_iot/tests/product/test_command_test_show_int.py new file mode 100644 index 000000000..88d69ec04 --- /dev/null +++ b/azext_iot/tests/product/test_command_test_show_int.py @@ -0,0 +1,23 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from . import AICSLiveScenarioTest + + +class TestTestShowInt(AICSLiveScenarioTest): + def __init__(self, test_case): + self.test_id = "524ac74f-752b-4748-9667-45cd09e8a098" + super(TestTestShowInt, self).__init__(test_case) + + def test_show_test(self): + # call the GET /deviceTest/{test_id} + output = self.cmd( + "iot product test show -t {} --base-url {}".format( + self.test_id, self.kwargs["BASE_URL"] + ) + ).get_output_in_json() + + assert output["id"] == self.test_id diff --git a/azext_iot/tests/product/test_command_test_update_int.py b/azext_iot/tests/product/test_command_test_update_int.py new file mode 100644 index 000000000..c2ee221ef --- /dev/null +++ b/azext_iot/tests/product/test_command_test_update_int.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from . import AICSLiveScenarioTest +from azext_iot.product.shared import AttestationType + + +class TestTestUpdateInt(AICSLiveScenarioTest): + def __init__(self, test_case): + self.test_id = "524ac74f-752b-4748-9667-45cd09e8a098" + super(TestTestUpdateInt, self).__init__(test_case) + + def test_update_symmetric_key(self): + # call the GET /deviceTest/{test_id} + output = self.cmd( + "iot product test update -t {} --at symmetricKey --base-url {}".format( + self.test_id, self.kwargs["BASE_URL"] + ) + ).get_output_in_json() + + assert output["id"] == self.test_id + assert ( + output["provisioningConfiguration"]["type"] + == AttestationType.symmetricKey.value + ) + assert output["provisioningConfiguration"]["symmetricKeyEnrollmentInformation"] diff --git a/azext_iot/tests/product/test_command_test_update_unit.py b/azext_iot/tests/product/test_command_test_update_unit.py new file mode 100644 index 000000000..649163755 --- /dev/null +++ b/azext_iot/tests/product/test_command_test_update_unit.py @@ -0,0 +1,411 @@ +# 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 unittest +import mock +from knack.util import CLIError +from azext_iot.product.test.command_tests import update +from azext_iot.product.shared import BadgeType, AttestationType + + +class TestTestUpdateUnit(unittest.TestCase): + def __init__(self, test_case): + self.test_id = "3beb0e67-33d0-4896-b69b-91c7b7ce8fab" + super(TestTestUpdateUnit, self).__init__(test_case) + + def test_update_with_x509_and_no_certificate_fails(self): + with self.assertRaises(CLIError) as context: + update( + self, test_id=self.test_id, attestation_type=AttestationType.x509.value + ) + + self.assertEqual( + "If attestation type is x509, certificate path is required", + str(context.exception), + ) + + def test_update_with_tpm_and_no_endorsement_key_fails(self): + with self.assertRaises(CLIError) as context: + update( + self, test_id=self.test_id, attestation_type=AttestationType.tpm.value + ) + + self.assertEqual( + "If attestation type is tpm, endorsement key is required", + str(context.exception), + ) + + def test_update_with_pnp_and_no_models_fails(self): + with self.assertRaises(CLIError) as context: + update(self, test_id=self.test_id, badge_type=BadgeType.Pnp.value) + + self.assertEqual( + "If badge type is Pnp, models is required", str(context.exception) + ) + + def test_edge_module_without_connection_string_fails(self): + with self.assertRaises(CLIError) as context: + update( + self, + test_id=self.test_id, + attestation_type=AttestationType.connectionString.value, + badge_type=BadgeType.IotEdgeCompatible.value, + ) + + self.assertEqual( + "Connection string is required for Edge Compatible modules testing", + str(context.exception), + ) + + def test_connection_string_for_pnp_fails(self): + with self.assertRaises(CLIError) as context: + update( + self, + test_id=self.test_id, + attestation_type=AttestationType.connectionString.value, + badge_type=BadgeType.Pnp.value, + models="./stuff", + ) + + self.assertEqual( + "Connection string is only available for Edge Compatible modules testing", + str(context.exception), + ) + + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.update_device_test") + @mock.patch("azext_iot.product.test.command_tests._create_from_file") + def test_update_from_file(self, mock_from_file, mock_sdk_update): + mock_file_data = {"mock": "data"} + mock_from_file.return_value = mock_file_data + update(self, test_id=self.test_id, configuration_file="somefile") + mock_from_file.assert_called_with("somefile") + mock_sdk_update.assert_called_with( + device_test_id=self.test_id, + generate_provisioning_configuration=False, + body=mock_file_data, + raw=True, + ) + + @mock.patch("azext_iot.product.test.command_tests._read_certificate_from_file") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.update_device_test") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.get_device_test") + def test_update_symmetric_key_to_cert( + self, mock_from_get, mock_sdk_update, mock_read_certificate + ): + mock_read_certificate.return_value = "MockBase64String" + mock_test_data = { + "certificationBadgeConfigurations": [{"type": "IotDevice"}], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "deviceConnectionString": None, + "deviceId": "device_1234", + "dpsRegistrationId": "DPS_1234", + "region": "region_1", + "symmetricKeyEnrollmentInformation": { + "primaryKey": "primary_key", + "registrationId": "registration_1234", + "scopeId": "scope_1", + "secondaryKey": "secondary_key", + }, + "tpmEnrollmentInformation": None, + "type": "SymmetricKey", + "x509EnrollmentInformation": None, + }, + "validationType": "Certification", + } + return_data = mock.Mock() + return_data.response.json.return_value = mock_test_data + + mock_from_get.return_value = return_data + update( + self, + test_id=self.test_id, + attestation_type=AttestationType.x509.value, + certificate_path="mycertificate.cer", + ) + mock_read_certificate.assert_called_with("mycertificate.cer") + mock_sdk_update.assert_called_with( + device_test_id=self.test_id, + generate_provisioning_configuration=True, + raw=True, + body={ + "certificationBadgeConfigurations": [{"type": "IotDevice"}], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "dpsRegistrationId": "DPS_1234", + "type": AttestationType.x509.value, + "x509EnrollmentInformation": { + "base64EncodedX509Certificate": "MockBase64String" + }, + }, + "validationType": "Certification", + }, + ) + + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.update_device_test") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.get_device_test") + def test_update_symmetric_key_to_tpm(self, mock_from_get, mock_sdk_update): + mock_test_data = { + "certificationBadgeConfigurations": [{"type": "IotDevice"}], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "deviceConnectionString": None, + "deviceId": "device_1234", + "dpsRegistrationId": "DPS_1234", + "region": "region_1", + "symmetricKeyEnrollmentInformation": { + "primaryKey": "primary_key", + "registrationId": "registration_1234", + "scopeId": "scope_1", + "secondaryKey": "secondary_key", + }, + "tpmEnrollmentInformation": None, + "type": "SymmetricKey", + "x509EnrollmentInformation": None, + }, + "validationType": "Certification", + } + return_data = mock.Mock() + return_data.response.json.return_value = mock_test_data + + mock_from_get.return_value = return_data + update( + self, + test_id=self.test_id, + attestation_type=AttestationType.tpm.value, + endorsement_key="endorsement_key", + ) + mock_sdk_update.assert_called_with( + device_test_id=self.test_id, + generate_provisioning_configuration=True, + raw=True, + body={ + "certificationBadgeConfigurations": [{"type": "IotDevice"}], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "dpsRegistrationId": "DPS_1234", + "type": AttestationType.tpm.value, + "tpmEnrollmentInformation": {"endorsementKey": "endorsement_key"}, + }, + "validationType": "Certification", + }, + ) + + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.update_device_test") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.get_device_test") + def test_update_symmetric_key_to_symmetric_key( + self, mock_from_get, mock_sdk_update + ): + mock_test_data = { + "certificationBadgeConfigurations": [{"type": "IotDevice"}], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "deviceConnectionString": None, + "deviceId": "device_1234", + "dpsRegistrationId": "DPS_1234", + "region": "region_1", + "symmetricKeyEnrollmentInformation": { + "primaryKey": "primary_key", + "registrationId": "registration_1234", + "scopeId": "scope_1", + "secondaryKey": "secondary_key", + }, + "tpmEnrollmentInformation": None, + "type": "SymmetricKey", + "x509EnrollmentInformation": None, + }, + "validationType": "Certification", + } + return_data = mock.Mock() + return_data.response.json.return_value = mock_test_data + + mock_from_get.return_value = return_data + update( + self, + test_id=self.test_id, + attestation_type=AttestationType.symmetricKey.value, + ) + mock_sdk_update.assert_called_with( + device_test_id=self.test_id, + generate_provisioning_configuration=True, + raw=True, + body={ + "certificationBadgeConfigurations": [{"type": "IotDevice"}], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "dpsRegistrationId": "DPS_1234", + "type": AttestationType.symmetricKey.value, + "symmetricKeyEnrollmentInformation": {}, + }, + "validationType": "Certification", + }, + ) + + @mock.patch("azext_iot.product.test.command_tests._process_models_directory") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.update_device_test") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.get_device_test") + def test_update_iotdevice_to_pnp( + self, mock_from_get, mock_sdk_update, mock_process_models + ): + mock_process_models.return_value = [ + '{"@id":"model1"}', + '{"@id":"model2"}', + '{"@id":"model3"}', + ] + mock_test_data = { + "certificationBadgeConfigurations": [{"type": "IotDevice"}], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "deviceConnectionString": None, + "deviceId": "device_1234", + "dpsRegistrationId": "DPS_1234", + "region": "region_1", + "symmetricKeyEnrollmentInformation": { + "primaryKey": "primary_key", + "registrationId": "registration_1234", + "scopeId": "scope_1", + "secondaryKey": "secondary_key", + }, + "tpmEnrollmentInformation": None, + "type": "SymmetricKey", + "x509EnrollmentInformation": None, + }, + "validationType": "Certification", + } + return_data = mock.Mock() + return_data.response.json.return_value = mock_test_data + + mock_from_get.return_value = return_data + update( + self, + test_id=self.test_id, + badge_type=BadgeType.Pnp.value, + models="model_folder", + ) + mock_process_models.assert_called_with("model_folder") + mock_sdk_update.assert_called_with( + device_test_id=self.test_id, + generate_provisioning_configuration=False, + raw=True, + body={ + "certificationBadgeConfigurations": [ + { + "type": BadgeType.Pnp.value, + "digitalTwinModelDefinitions": [ + '{"@id":"model1"}', + '{"@id":"model2"}', + '{"@id":"model3"}', + ], + } + ], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "deviceConnectionString": None, + "deviceId": "device_1234", + "dpsRegistrationId": "DPS_1234", + "region": "region_1", + "symmetricKeyEnrollmentInformation": { + "primaryKey": "primary_key", + "registrationId": "registration_1234", + "scopeId": "scope_1", + "secondaryKey": "secondary_key", + }, + "tpmEnrollmentInformation": None, + "type": "SymmetricKey", + "x509EnrollmentInformation": None, + }, + "validationType": "Certification", + }, + ) + + @mock.patch("azext_iot.product.test.command_tests._process_models_directory") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.update_device_test") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.get_device_test") + def test_update_pnp_to_iotdevice( + self, mock_from_get, mock_sdk_update, mock_process_models + ): + mock_test_data = { + "certificationBadgeConfigurations": [ + { + "type": BadgeType.Pnp.value, + "digitalTwinModelDefinitions": [ + '{"@id":"model1"}', + '{"@id":"model2"}', + '{"@id":"model3"}', + ], + } + ], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "deviceConnectionString": None, + "deviceId": "device_1234", + "dpsRegistrationId": "DPS_1234", + "region": "region_1", + "symmetricKeyEnrollmentInformation": { + "primaryKey": "primary_key", + "registrationId": "registration_1234", + "scopeId": "scope_1", + "secondaryKey": "secondary_key", + }, + "tpmEnrollmentInformation": None, + "type": "SymmetricKey", + "x509EnrollmentInformation": None, + }, + "validationType": "Certification", + } + return_data = mock.Mock() + return_data.response.json.return_value = mock_test_data + + mock_from_get.return_value = return_data + update(self, test_id=self.test_id, badge_type=BadgeType.IotDevice.value) + mock_process_models.assert_not_called() + mock_sdk_update.assert_called_with( + device_test_id=self.test_id, + generate_provisioning_configuration=False, + raw=True, + body={ + "certificationBadgeConfigurations": [ + {"type": BadgeType.IotDevice.value, } + ], + "deviceType": "DevKit", + "id": self.test_id, + "productId": "product_1234", + "provisioningConfiguration": { + "deviceConnectionString": None, + "deviceId": "device_1234", + "dpsRegistrationId": "DPS_1234", + "region": "region_1", + "symmetricKeyEnrollmentInformation": { + "primaryKey": "primary_key", + "registrationId": "registration_1234", + "scopeId": "scope_1", + "secondaryKey": "secondary_key", + }, + "tpmEnrollmentInformation": None, + "type": "SymmetricKey", + "x509EnrollmentInformation": None, + }, + "validationType": "Certification", + }, + ) diff --git a/azext_iot/tests/product/test_device_test_tasks_int.py b/azext_iot/tests/product/test_device_test_tasks_int.py new file mode 100644 index 000000000..8ca08d2b1 --- /dev/null +++ b/azext_iot/tests/product/test_device_test_tasks_int.py @@ -0,0 +1,82 @@ +# 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 time import sleep +from . import AICSLiveScenarioTest +from azext_iot.product.shared import TaskType, DeviceTestTaskStatus + + +class TestProductDeviceTestTasks(AICSLiveScenarioTest): + def __init__(self, _): + super(TestProductDeviceTestTasks, self).__init__(_) + self.kwargs.update( + { + "device_test_id": "524ac74f-752b-4748-9667-45cd09e8a098", + "generate_task": TaskType.GenerateTestCases.value, + "queue_task": TaskType.QueueTestRun.value, + } + ) + + def setup(self): + return True + + def teardown(self): + return True + + def test_product_device_test_tasks(self): + + # create task for GenerateTestCases + created = self.cmd( + "iot product test task create -t {device_test_id} --type {generate_task} --wait --base-url {BASE_URL}" + ).get_output_in_json() + assert created["deviceTestId"] == self.kwargs["device_test_id"] + assert json.dumps(created) + + test_task_id = created["id"] + self.kwargs.update({"device_test_task_id": test_task_id}) + + # show task + show = self.cmd( + "iot product test task show -t {device_test_id} --task-id {device_test_task_id} --base-url {BASE_URL}" + ).get_output_in_json() + assert json.dumps(show) + assert show["deviceTestId"] == self.kwargs["device_test_id"] + assert show["id"] == self.kwargs["device_test_task_id"] + + # Queue a test run without wait, get run_id + queue_task = self.cmd( + "iot product test task create -t {device_test_id} --type {queue_task} --base-url {BASE_URL}" + ).get_output_in_json() + assert queue_task["type"] == TaskType.QueueTestRun.value + assert queue_task["status"] == DeviceTestTaskStatus.queued.value + + self.kwargs.update({"queue_task_id": queue_task["id"]}) + + # allow test to start running + sleep(5) + + queue_task = self.cmd( + "iot product test task show -t {device_test_id} --task-id {queue_task_id} --base-url {BASE_URL}" + ).get_output_in_json() + assert queue_task["type"] == TaskType.QueueTestRun.value + assert queue_task["status"] != DeviceTestTaskStatus.queued.value + + if queue_task["status"] == DeviceTestTaskStatus.running: + # Cancel running test task + self.cmd( + "iot product test task delete -t {device_test_id} --task-id {queue_task_id} --base-url {BASE_URL}" + ) + + # allow test to be cancelled + sleep(5) + + # get cancelled test task + show = self.cmd( + "iot product test task show -t {device_test_id} --task-id {queue_task_id} --base-url {BASE_URL}" + ).get_output_in_json() + + assert show["status"] == DeviceTestTaskStatus.cancelled.value diff --git a/azext_iot/tests/product/test_device_test_tasks_unit.py b/azext_iot/tests/product/test_device_test_tasks_unit.py new file mode 100644 index 000000000..dae5e3197 --- /dev/null +++ b/azext_iot/tests/product/test_device_test_tasks_unit.py @@ -0,0 +1,345 @@ +# 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 unittest +import pytest +import mock +import json +import responses +from knack.util import CLIError +from azext_iot.sdk.product.models import DeviceTestTask, TestRun +from azext_iot.product.test.command_test_tasks import create, delete, show +from azext_iot.product.shared import TaskType +from azext_iot.product.shared import BASE_URL + +mock_target = {} +mock_target["entity"] = BASE_URL +device_test_id = "12345" +device_test_task_id = "54321" +device_test_run_id = "67890" +task_result = { + "id": device_test_task_id, + "status": "Queued", + "type": "QueueTestRun", + "deviceTestId": device_test_id, + "resultLink": "{}/testRuns/{}".format(device_test_id, device_test_run_id), +} + +run_result = { + "id": device_test_run_id, + "start_time": "start_time", + "end_time": "end_time", + "status": "Completed", + "certificationBadgeResults": [], +} + +queued_task = DeviceTestTask( + id=device_test_task_id, device_test_id=device_test_id, status="Queued" +) +started_task = DeviceTestTask( + id=device_test_task_id, device_test_id=device_test_id, status="Started" +) +running_task = DeviceTestTask( + id=device_test_task_id, device_test_id=device_test_id, status="Running" +) +completed_task = DeviceTestTask( + id=device_test_task_id, device_test_id=device_test_id, status="Completed", +) + +task_result_body = json.dumps(task_result) +finished_task_result_body = task_result_body.replace("Queued", "Completed") +api_string = "?api-version=2020-05-01-preview" + + +class TestTaskCreate(unittest.TestCase): + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test_task") + def test_task_create(self, mock_create): + create(self, test_id=device_test_id) + mock_create.assert_called_with( + device_test_id=device_test_id, task_type=TaskType.QueueTestRun.value + ) + + @mock.patch( + "azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test_task", + return_value=queued_task, + ) + @mock.patch( + "azext_iot.sdk.product.aicsapi.AICSAPI.get_device_test_task", + side_effect=iter([started_task, running_task, completed_task]), + ) + @mock.patch("time.sleep") + def test_task_create_wait(self, mock_sleep, mock_get, mock_create): + result = create(self, test_id=device_test_id, wait=True, poll_interval=1) + + # one call to create + mock_create.assert_called_with( + device_test_id=device_test_id, task_type=TaskType.QueueTestRun.value + ) + assert mock_create.call_count == 1 + + # three calls to 'get' until status is 'Completed' + mock_get.assert_called_with( + task_id=device_test_task_id, device_test_id=device_test_id + ) + assert mock_get.call_count == 3 + + # make sure we get the last result back + assert result.status == "Completed" + + @mock.patch( + "azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test_task", + return_value=queued_task, + ) + @mock.patch( + "azext_iot.sdk.product.aicsapi.AICSAPI.get_device_test_task", + side_effect=iter([started_task, running_task, completed_task]), + ) + def test_task_create_no_wait(self, mock_get, mock_create): + result = create(self, test_id=device_test_id, wait=False, poll_interval=1) + + # one call to create + mock_create.assert_called_with( + device_test_id=device_test_id, task_type=TaskType.QueueTestRun.value + ) + assert mock_create.call_count == 1 + + # no calls to 'get' since wait==Falce + mock_get.assert_not_called() + + # initial create response returned + assert result == queued_task + + @mock.patch( + "azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test_task", + return_value={"error": "task currently running"}, + ) + def test_task_create_failure(self, mock_create): + with self.assertRaises(CLIError) as context: + create(self, test_id=device_test_id, wait=False) + self.assertTrue({"error": "task currently running"}, context) + + @mock.patch( + "azext_iot.sdk.product.aicsapi.AICSAPI.create_device_test_task", + return_value=None, + ) + def test_task_create_empty_response(self, mock_create): + with self.assertRaises(CLIError) as context: + create(self, test_id=device_test_id, wait=False) + self.assertTrue( + "Failed to create device test task - please ensure a device test exists with Id {}".format( + device_test_id + ), + context.exception, + ) + + +class TestTaskShow(unittest.TestCase): + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.get_device_test_task") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.get_running_device_test_tasks") + def test_task_show_task(self, mock_get_running, mock_get_task): + show(self, test_id=device_test_id, task_id="456") + mock_get_task.assert_called_with(task_id="456", device_test_id=device_test_id) + self.assertEqual(mock_get_task.call_count, 1) + self.assertEqual(mock_get_running.call_count, 0) + + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.get_device_test_task") + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.get_running_device_test_tasks") + def test_task_show_running(self, mock_get_running, mock_get_task): + show(self, test_id=device_test_id, running=True) + mock_get_running.assert_called_with(device_test_id=device_test_id) + self.assertEqual(mock_get_running.call_count, 1) + self.assertEqual(mock_get_task.call_count, 0) + + def test_task_show_incorrect_params(self): + with self.assertRaises(CLIError) as context: + show(self, test_id=device_test_id) + self.assertTrue( + "Please provide a task-id for individual task details, or use the --running argument to list all running tasks", + context.exception, + ) + + +class TestTaskDelete(unittest.TestCase): + @mock.patch("azext_iot.sdk.product.aicsapi.AICSAPI.cancel_device_test_task") + def test_task_delete(self, mock_delete): + delete(self, test_id=device_test_id, task_id="234") + assert mock_delete.call_count == 1 + mock_delete.assert_called_with(task_id="234", device_test_id=device_test_id) + + +class TestTasksSDK(object): + + # create call + @pytest.fixture(params=[202]) + def service_client_create(self, mocked_response, fixture_mock_aics_token, request): + + # create test task + mocked_response.add( + method=responses.POST, + url="{}/deviceTests/{}/tasks{}".format( + mock_target["entity"], device_test_id, api_string + ), + body=task_result_body, + headers={"x-ms-command-statuscode": str(request.param)}, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response + + # create task, get task, get run (for --wait) + @pytest.fixture(params=[200]) + def service_client_create_wait( + self, service_client_create, mocked_response, fixture_mock_aics_token, request + ): + mocked_response.add( + method=responses.GET, + url="{}/deviceTests/{}/tasks/{}{}".format( + mock_target["entity"], device_test_id, device_test_task_id, api_string + ), + body=finished_task_result_body, + headers={"x-ms-command-statuscode": str(request.param)}, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + # get completed queued test run + mocked_response.add( + method=responses.GET, + url="{}/deviceTests/{}/testRuns/{}{}".format( + mock_target["entity"], device_test_id, device_test_run_id, api_string + ), + body=json.dumps(run_result), + headers={"x-ms-command-statuscode": str(200)}, + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response + + # delete task + @pytest.fixture(params=[202]) + def service_client_delete(self, mocked_response, fixture_mock_aics_token, request): + mocked_response.add( + method=responses.DELETE, + url="{}/deviceTests/{}/tasks/{}{}".format( + mock_target["entity"], device_test_id, device_test_task_id, api_string + ), + body="{}", + headers={"x-ms-command-statuscode": str(request.param)}, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + # get single task + @pytest.fixture(params=[200]) + def service_client_get(self, mocked_response, fixture_mock_aics_token, request): + + mocked_response.add( + method=responses.GET, + url="{}/deviceTests/{}/tasks/{}{}".format( + mock_target["entity"], device_test_id, device_test_task_id, api_string + ), + body=task_result_body, + headers={"x-ms-command-statuscode": str(request.param)}, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response + + # get running tasks + @pytest.fixture(params=[200]) + def service_client_get_running( + self, mocked_response, fixture_mock_aics_token, request + ): + mocked_response.add( + method=responses.GET, + url="{}/deviceTests/{}/tasks/running{}".format( + mock_target["entity"], device_test_id, api_string + ), + body="[{}]".format(task_result_body), + headers={"x-ms-command-statuscode": str(request.param)}, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response + + def test_sdk_task_create(self, fixture_cmd, service_client_create): + result = create(fixture_cmd, test_id=device_test_id) + req = service_client_create.calls[0].request + + assert req.method == "POST" + assert json.loads(req.body)["taskType"] == "QueueTestRun" + assert result.id == device_test_task_id + assert result.device_test_id == device_test_id + + def test_sdk_task_create_wait(self, fixture_cmd, service_client_create_wait): + result = create(fixture_cmd, test_id=device_test_id, wait=True, poll_interval=1) + reqs = list(map(lambda call: call.request, service_client_create_wait.calls)) + + # Call 0 - create test task + assert reqs[0].method == "POST" + assert json.loads(reqs[0].body)["taskType"] == "QueueTestRun" + url = reqs[0].url + assert "deviceTests/{}/tasks".format(device_test_id) in url + + # Call 1 - get task status + assert reqs[1].method == "GET" + url = reqs[1].url + assert ( + "deviceTests/{}/tasks/{}".format(device_test_id, device_test_task_id) in url + ) + + # Call 2 - get run results + assert reqs[2].method == "GET" + url = reqs[2].url + assert ( + "deviceTests/{}/testRuns/{}".format(device_test_id, device_test_run_id) + in url + ) + + # awaiting a queued test run should yield a test run object + assert isinstance(result, TestRun) + assert result.id == device_test_run_id + assert result.status == "Completed" + + def test_sdk_task_delete(self, fixture_cmd, service_client_delete): + result = delete( + fixture_cmd, test_id=device_test_id, task_id=device_test_task_id + ) + assert not result + + req = service_client_delete.calls[0].request + url = req.url + assert req.method == "DELETE" + assert ( + "deviceTests/{}/tasks/{}".format(device_test_id, device_test_task_id) in url + ) + + def test_sdk_task_show_task(self, fixture_cmd, service_client_get): + result = show(fixture_cmd, test_id=device_test_id, task_id=device_test_task_id) + req = service_client_get.calls[0].request + url = req.url + assert ( + "deviceTests/{}/tasks/{}".format(device_test_id, device_test_task_id) in url + ) + assert req.method == "GET" + assert result.id == device_test_task_id + assert result.device_test_id == device_test_id + + def test_sdk_task_show_running(self, fixture_cmd, service_client_get_running): + result = show(fixture_cmd, test_id=device_test_id, running=True) + req = service_client_get_running.calls[0].request + url = req.url + assert "deviceTests/{}/tasks/running".format(device_test_id) in url + assert req.method == "GET" + assert result[0].id == device_test_task_id + assert result[0].device_test_id == device_test_id From c925aa206db57046c70ce0af77d58e0e4e1999e4 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 11 Aug 2020 16:28:20 -0700 Subject: [PATCH 088/179] Update HISTORY.rst --- HISTORY.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 6eeb41af6..87843e5a5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,34 @@ Release History =============== +0.9.7 ++++++++++++++++ +Refreshes commands for the Azure IoT Plug & Play summer refresh + +* The existing Plug & Play preview commands across Azure CLI and the IoT extension have been removed and replaced with a completely new commands. If you still need the legacy preview experience, then you can leverage older versions of the CLI and extension. +* The new commands exist entirely in the extension with the following command groups: + + * az iot pnp repo ## For tenant repository configuration + * az iot pnp model ## For managing repository models and related content + * az iot pnp role-assignment ## For managing role assignments for model repo assets + * az iot pnp twin ## For interacting with the digital twin of a Plug & Play device + +Introduces new preview Azure IoT Central commands + +* az iot central app monitor-properties +* az iot central app validate-properties +* az iot central app device run-command +* az iot central app device show-command-history +* az iot central app device show-credentials + +Device Provisioning Service update + +* DPS enrollments now support the custom allocation policy resolving issue #200 + +0.9.6 ++++++++++++++++ +* Fixes event monitor initialization issue. + 0.9.5 +++++++++++++++ * IoT Hub commands now support dynamic privileged policy discovery. `iothubhowner` is no longer relied on. Instead any policy that has `RegistryWrite`, `ServiceConnect` and `DeviceConnect` permissions will be used. From 3e814e566a5a9b01c645ec8f3ba4d45638423c1c Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 11 Aug 2020 18:45:28 -0700 Subject: [PATCH 089/179] Update CODEOWNERS, PR template and edge deployment help. --- .github/CODEOWNERS | 16 ++++++++++++---- .github/PULL_REQUEST_TEMPLATE.md | 1 + azext_iot/_help.py | 24 ++++++++++++++++++------ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 68f7f0398..78122037e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,13 +6,21 @@ # 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 -# Tests Code Owners -azext_iot/tests/central/ @prbans -azext_iot/tests/test_iot_central_int.py @prbans -azext_iot/tests/test_iot_central_unit.py @prbans +# 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/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3ee7a39e1..b064e1fcf 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,3 +11,4 @@ This checklist is used to make sure that common guidelines for a pull request ar - [ ] In the same context as above are command names and their parameter definitions accurate? Do help docs have sufficient content? - [ ] Have **all** unit **and** integration tests passed locally? i.e. `pytest -vv` - [ ] Have static checks passed using the .pylintrc and .flake8 rules? Look at the CI scripts for example usage. +- [ ] Have you made an entry in HISTORY.rst which concisely explains your feature or change? diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 96a55a98b..b0c82d382 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -864,25 +864,37 @@ - name: Create a deployment with labels (bash syntax example) that applies for devices in 'building 9' and the environment is 'test'. text: > - az iot edge deployment create -d {deployment_name} -n {iothub_name} --content modules_content.json + az iot edge deployment create -d {deployment_name} -n {iothub_name} + --content modules_content.json --labels '{"key0":"value0", "key1":"value1"}' --target-condition "tags.building=9 and tags.environment='test'" --priority 3 - name: Create a deployment with labels (powershell syntax example) that applies for devices tagged with environment 'dev'. text: > - az iot edge deployment create -d {deployment_name} -n {iothub_name} --content modules_content.json + az iot edge deployment create -d {deployment_name} -n {iothub_name} + --content modules_content.json --labels '{\\"key\\":\\"value\\"}' --target-condition "tags.environment='dev'" - name: Create a layered deployment that applies for devices tagged with environment 'dev'. - Both user metrics and modules content defined inline (cmd syntax example). + Both user metrics and modules content defined inline (powershell syntax example). text: > - az iot edge deployment create -d {deployment_name} -n {iothub_name} + az --% iot edge deployment create -d {deployment_name} -n {iothub_name} --content "{\\"modulesContent\\":{\\"$edgeAgent\\":{\\"properties.desired.modules.mymodule0\\":{ }},\\"$edgeHub\\":{\\"properties.desired.routes.myroute0\\":\\"FROM /messages/* INTO $upstream\\"}}}" --target-condition "tags.environment='dev'" --priority 10 --metrics "{\\"queries\\":{\\"mymetrik\\":\\"SELECT deviceId from devices where properties.reported.lastDesiredStatus.code = 200\\"}}" --layered - - name: Create a layered deployment that applies for devices in 'building 9' and the environment is 'test'. + - name: Create a layered deployment that applies for devices in 'building 9' and environment 'test'. + Both user metrics and modules content defined inline (bash syntax example). + text: > + az iot edge deployment create -d {deployment_name} -n {iothub_name} + --content '{"modulesContent":{"$edgeAgent":{"properties.desired.modules.mymodule0":{ }},"$edgeHub":{"properties.desired.routes.myroute0":"FROM /messages/* INTO $upstream"}}}' + --target-condition "tags.building=9 and tags.environment='test'" + --metrics '{"queries":{"mymetrik":"SELECT deviceId from devices where properties.reported.lastDesiredStatus.code = 200"}}' + --layered + - name: Create a layered deployment that applies for devices in 'building 9' and environment 'test'. + Both user metrics and modules content defined from file. text: > - az iot edge deployment create -d {deployment_name} -n {iothub_name} --content layered_modules_content.json + az iot edge deployment create -d {deployment_name} -n {iothub_name} + --content layered_modules_content.json --target-condition "tags.building=9 and tags.environment='test'" --metrics metrics_content.json --layered From 7ebe9481d4329b888019481a966542d87aa567f5 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 11 Aug 2020 19:16:54 -0700 Subject: [PATCH 090/179] Alternate edge example to avoid linter flag. --- azext_iot/_help.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index b0c82d382..a18cec615 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -867,20 +867,22 @@ az iot edge deployment create -d {deployment_name} -n {iothub_name} --content modules_content.json --labels '{"key0":"value0", "key1":"value1"}' - --target-condition "tags.building=9 and tags.environment='test'" --priority 3 + --target-condition "tags.building=9 and tags.environment='test'" + --priority 3 - name: Create a deployment with labels (powershell syntax example) that applies for devices tagged with environment 'dev'. text: > az iot edge deployment create -d {deployment_name} -n {iothub_name} --content modules_content.json - --labels '{\\"key\\":\\"value\\"}' + --labels "{'key':'value'}" --target-condition "tags.environment='dev'" - name: Create a layered deployment that applies for devices tagged with environment 'dev'. Both user metrics and modules content defined inline (powershell syntax example). text: > - az --% iot edge deployment create -d {deployment_name} -n {iothub_name} - --content "{\\"modulesContent\\":{\\"$edgeAgent\\":{\\"properties.desired.modules.mymodule0\\":{ }},\\"$edgeHub\\":{\\"properties.desired.routes.myroute0\\":\\"FROM /messages/* INTO $upstream\\"}}}" - --target-condition "tags.environment='dev'" --priority 10 - --metrics "{\\"queries\\":{\\"mymetrik\\":\\"SELECT deviceId from devices where properties.reported.lastDesiredStatus.code = 200\\"}}" + az iot edge deployment create -d {deployment_name} -n {iothub_name} + --content "{'modulesContent':{'`$edgeAgent':{'properties.desired.modules.mymodule0':{ }},'`$edgeHub':{'properties.desired.routes.myroute0':'FROM /messages/* INTO `$upstream'}}}" + --target-condition "tags.environment='dev'" + --priority 10 + --metrics "{'queries':{'mymetrik':'SELECT deviceId from devices where properties.reported.lastDesiredStatus.code = 200'}}" --layered - name: Create a layered deployment that applies for devices in 'building 9' and environment 'test'. Both user metrics and modules content defined inline (bash syntax example). From d1e154937fb103d90626c4d1db9583ced8b2bd17 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 12 Aug 2020 12:10:13 -0700 Subject: [PATCH 091/179] Update HISTORY.rst --- HISTORY.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 87843e5a5..05c11375c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,36 @@ Release History =============== +0.9.8 ++++++++++++++++ +Introducing commands for the Azure IoT Product Certification service + +* A new IoT root command group 'az iot product' has been added + + * Use 'az iot product requirement' to manage product certification requirements + * Use 'az iot product test' to manage device tests for certification + + * The product test command group encompasses test cases, runs and tasks + +IoT Central updates + +* Introduces the 'az iot central app user' command group for managing application users and service principals +* Introduces the 'az iot central app api-token' command group for managing application api tokens +* Removal of deprecated command groups and commands + +IoT Hub updates + +* All "... show-connection-string" based commands are deprecated in favor of "... connection-string show" canonical Az CLI style. + + * The show connection string command for a target IoT Hub has moved to the IoT extension. + * 'az iot hub connection-string show' supports a --default-eventhub flag which indicates the operation will construct a connection string for the default eventhub endpoint of the target IoT Hub. +* Export/Import device identity commands support reading blob container SAS URI's via file + +Azure Digital Twins updates + +* The 'location' argument for 'az dt create' is now optional. If no location is provided, the location of the target resource group is used. + + 0.9.7 +++++++++++++++ Refreshes commands for the Azure IoT Plug & Play summer refresh From cbe1879ef21fb2354678b92ba6de0ff60e976375 Mon Sep 17 00:00:00 2001 From: Paul Montgomery Date: Wed, 12 Aug 2020 16:13:25 -0700 Subject: [PATCH 092/179] Set iot product command group to preview (#233) --- azext_iot/product/command_map.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/azext_iot/product/command_map.py b/azext_iot/product/command_map.py index 25dba78b7..1bbee92d4 100644 --- a/azext_iot/product/command_map.py +++ b/azext_iot/product/command_map.py @@ -19,6 +19,11 @@ def load_product_commands(self, _): + with self.command_group( + "iot product", command_type=requirements_ops, is_preview=True + ) as g: + pass + with self.command_group( "iot product requirement", command_type=requirements_ops ) as g: From 980d421e25449091e19426d29117ac48280a2f20 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 12 Aug 2020 16:29:59 -0700 Subject: [PATCH 093/179] Readme improvements (#234) * Shows collapsible sections for all supported IoT services. * IoT Central starts expanded. --- README.md | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 132 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5510ece54..6f92f0b70 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ ![Python](https://img.shields.io/pypi/pyversions/azure-cli.svg?maxAge=2592000) ![Build Status](https://dev.azure.com/azureiotdevxp/aziotcli/_apis/build/status/Merge%20-%20Azure.azure-iot-cli-extension?branchName=dev) - 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 @@ -20,8 +19,7 @@ cliextensions/azure-cli-iot-ext/azext_iot/_factory.py, ln 29, in iot_hub_service 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 @@ -42,7 +40,113 @@ Please refer to the [Installation Troubleshooting Guide](docs/install-help.md) i After installing the Azure IoT extension your CLI environment is augmented with the addition of `central`, `device`, `dps`, `dt`, `edge`, `hub` and `pnp` commands. -For usage and help content for any command or command group, pass in the `-h` parameter, for example: +For usage and help content of any command or command group, pass in the `-h` parameter. Root command group details are shown for following IoT services. + +> **Click** a section to expand or collapse + +
+ Digital Twins + +``` +$ az dt -h +Group + az dt : Manage Azure Digital Twins solutions & infrastructure. + This command group is in preview. It may be changed/removed in a future release. +Subgroups: + endpoint : Manage and configure Digital Twins instance endpoints. + model : Manage DTDL models and definitions on a Digital Twins instance. + role-assignment : Manage RBAC role assignments for a Digital Twins instance. + route : Manage and configure event routes. + twin : Manage and configure the digital twins of a Digital Twins instance. + +Commands: + create : Create a new Digital Twins instance. + delete : Delete an existing Digital Twins instance. + list : List the collection of Digital Twins instances by subscription or resource + group. + show : Show an existing Digital Twins instance. +``` +
+ +
+ IoT Central + +``` +$ az iot central app -h +Group + az iot central app : Manage Azure IoT Central applications. + + To use this command group, the user must be logged through the `az login` command, + have the correct tenant set (the users home tenant) and + have access to the application through http://apps.azureiotcentral.com". + +Subgroups: + api-token [Preview] : Create and Manage API tokens . + device [Preview] : Manage and configure IoT Central devices. + device-template [Preview] : Manage and configure IoT Central device templates. + device-twin : Manage IoT Central device twins. + user [Preview] : Manage and configure IoT Central users. + +Commands: + create : Create an IoT Central application. + delete : Delete an IoT Central application. + list : List IoT Central applications. + monitor-events : Monitor device telemetry & messages sent to the IoT Hub for an + IoT Central app. + monitor-properties [Preview] : Monitor desired and reported properties sent to/from + the IoT Hub for an IoT Central app. + show : Get the details of an IoT Central application. + update : Update metadata for an IoT Central application. + validate-messages [Preview] : Validate messages sent to the IoT Hub for an IoT + Central app. + validate-properties [Preview] : Validate reported properties sent to IoT Central app. +``` +
+ +
+ IoT Device Provisioning Service + +``` +$ az iot dps -h +Group + az iot dps : Manage entities in an Azure IoT Hub Device Provisioning Service. Augmented with the + IoT extension. + +Subgroups: + access-policy : Manage Azure IoT Hub Device Provisioning Service access policies. + certificate : Manage Azure IoT Hub Device Provisioning Service certificates. + enrollment : Manage enrollments in an Azure IoT Hub Device Provisioning Service. + enrollment-group : Manage Azure IoT Hub Device Provisioning Service. + linked-hub : Manage Azure IoT Hub Device Provisioning Service linked IoT hubs. + registration : Manage Azure IoT Hub Device Provisioning Service registrations. + +Commands: + create : Create an Azure IoT Hub device provisioning service. + delete : Delete an Azure IoT Hub device provisioning service. + list : List Azure IoT Hub device provisioning services. + show : Get the details of an Azure IoT Hub device provisioning service. + update : Update an Azure IoT Hub device provisioning service. +``` +
+ +
+ IoT Edge + +``` +$ az iot edge -h +Group + az iot edge : Manage IoT solutions on the Edge. + +Subgroups: + deployment : Manage IoT Edge deployments at scale. + +Commands: + set-modules : Set edge modules on a single device. +``` +
+ +
+ IoT Hub ``` $ az iot hub -h @@ -51,13 +155,14 @@ Group Subgroups: certificate : Manage IoT Hub certificates. - configuration : Manage IoT device configurations at scale. + configuration : Manage IoT automatic device management configuration at scale. + connection-string : Manage IoT Hub connection strings. consumer-group : Manage the event hub consumer groups of an IoT hub. device-identity : Manage IoT devices. device-twin : Manage IoT device twin configuration. devicestream : Manage device streams of an IoT hub. distributed-tracing [Preview] : Manage distributed settings per-device. - job : Manage jobs in an IoT hub. + job : Manage IoT Hub jobs (v2). message-enrichment : Manage message enrichments for endpoints of an IoT Hub. module-identity : Manage IoT device modules. module-twin : Manage IoT device module twin configuration. @@ -85,6 +190,27 @@ Commands: show-stats : Get the statistics for an IoT hub. update : Update metadata for an IoT hub. ``` +
+ +
+ IoT Plug & Play + +``` +$ az iot pnp -h +Group + az iot pnp : Manage Azure IoT Plug-and-Play repositories and models. + +Subgroups: + model [Preview] : Create, view, and publish device models in your company + repository. + repo [Preview] : Create and view Azure IoT Plug-and-Play tenant + repositories. + role-assignment [Preview] : Manage and configure PnP repository and model role + assignments. + twin [Preview] : Manipulate and interact with the digital twin of an IoT + Hub device. +``` +
## Scenario Automation From 21ce2ddcd7b85e6ed84aac61ca53b3667865a840 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 12 Aug 2020 16:31:42 -0700 Subject: [PATCH 094/179] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f92f0b70..de86b6ea3 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Please refer to the [Installation Troubleshooting Guide](docs/install-help.md) i After installing the Azure IoT extension your CLI environment is augmented with the addition of `central`, `device`, `dps`, `dt`, `edge`, `hub` and `pnp` commands. -For usage and help content of any command or command group, pass in the `-h` parameter. Root command group details are shown for following IoT services. +For usage and help content of any command or command group, pass in the `-h` parameter. Root command group details are shown for the following IoT services. > **Click** a section to expand or collapse From 0a26a29a979e1cdf52e2cb0a6c3f28fb59e4887d Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 12 Aug 2020 17:09:43 -0700 Subject: [PATCH 095/179] Fixes/improvement to help and error of config show-metric. (#235) --- azext_iot/_help.py | 5 ++++- azext_iot/operations/hub.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index a18cec615..85bbcab93 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -949,7 +949,10 @@ examples: - name: Evaluate the 'appliedCount' system metric text: > - az iot edge deployment show-metric -m appliedCount -d {deployment_name} -n {iothub_name} + az iot edge deployment show-metric -m appliedCount -d {deployment_name} -n {iothub_name} --mt system + - name: Evaluate the 'myCustomMetric' user metric + text: > + az iot edge deployment show-metric -m myCustomMetric -d {deployment_name} -n {iothub_name} """ helps[ diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index ab3a988e9..9e2255b51 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -1254,8 +1254,8 @@ def iot_hub_configuration_metric_show( if metric_id not in metric_collection: raise CLIError( - "the metric '{}' is not defined in the device configuration '{}'".format( - metric_id, config_id + "The {} metric '{}' is not defined in the configuration '{}'".format( + metric_type, metric_id, config_id ) ) From 302a5cf3db3f3487dee00259e3e7ed5811fefa95 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 12 Aug 2020 17:16:09 -0700 Subject: [PATCH 096/179] Update HISTORY.rst --- HISTORY.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 05c11375c..04bdf9c87 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,7 +5,11 @@ Release History 0.9.8 +++++++++++++++ -Introducing commands for the Azure IoT Product Certification service +General changes + +* Starting with v0.9.8 of the IoT extension, the minCliCoreVersion has been bumped to 2.3.1. This sets a comfortable minimum desired experience we want for our users. + +Introducing preview commands for the Azure IoT Product Certification service * A new IoT root command group 'az iot product' has been added @@ -16,8 +20,8 @@ Introducing commands for the Azure IoT Product Certification service IoT Central updates -* Introduces the 'az iot central app user' command group for managing application users and service principals -* Introduces the 'az iot central app api-token' command group for managing application api tokens +* Introduces the 'az iot central app user' preview command group for managing application users and service principals +* Introduces the 'az iot central app api-token' preview command group for managing application api tokens * Removal of deprecated command groups and commands IoT Hub updates From bcb654a19858260fa54ae3f707f98b1a3d1d73c7 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Wed, 19 Aug 2020 11:42:15 -0700 Subject: [PATCH 097/179] CI tweak. Clone dev branch. --- .azure-devops/templates/install-configure-azure-cli.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-devops/templates/install-configure-azure-cli.yml b/.azure-devops/templates/install-configure-azure-cli.yml index e23a11e99..59bd54559 100644 --- a/.azure-devops/templates/install-configure-azure-cli.yml +++ b/.azure-devops/templates/install-configure-azure-cli.yml @@ -7,7 +7,7 @@ steps: pip install virtualenv python -m virtualenv $(iot_ext_venv)/ source ./$(iot_ext_venv)/bin/activate - git clone --single-branch -b master https://github.com/Azure/azure-cli.git ../azure-cli + git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli pip install azdev azdev --version azdev setup -c ../azure-cli From 36fafb0cb6bdbbeb13991d69d6363301c3d5333f Mon Sep 17 00:00:00 2001 From: valluriraj Date: Wed, 19 Aug 2020 18:55:50 -0700 Subject: [PATCH 098/179] Central command re-group (#237) * readme update, lint and help fixes * update history * address review comments --- HISTORY.rst | 18 ++ README.md | 41 ++-- azext_iot/central/_help.py | 299 +++++++++++++++--------- azext_iot/central/command_map.py | 74 ++++-- azext_iot/central/params.py | 131 ++++++----- azext_iot/tests/test_iot_central_int.py | 126 +++++++--- 6 files changed, 459 insertions(+), 230 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 04bdf9c87..5d3d55930 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,24 @@ Release History =============== +0.9.9 ++++++++++++++++ + +IoT Central updates +* Current release involves re-grouping of IoT central commands. + * 'az iot central app device-twin' deprecated use 'az iot central device twin' instead. Deprecated command group is planned to be removed by December 2020 + * 'az iot central app monitor-events' deprecated use 'az iot central diagnostics monitor-events' instead. Deprecated command is planned to be removed by December 2020 + * commands listed below are in preview + * Introduces 'az iot central diagnostics' preview command group to perform application and device level diagnostics + * 'az iot central app device registration-summary' moved to 'az iot central diagnostics registration-summary' + * 'az iot central app monitor-properties' moved to 'az iot central diagnostics monitor-properties' + * 'az iot central app validate-messages' moved to 'az iot central diagnostics validate-messages' + * 'az iot central app validate-properties' moved to 'az iot central diagnostics validate-properties' + * 'az iot central diagnostics monitor-events' added to support deprecation of 'az iot central app monitor-events' + * 'az iot central app device run-command' moved to 'az iot central device command run' + * 'az iot central app device show-command-history' moved to 'az iot central device command history' + * 'az iot central device twin' added to support deprecation of 'az iot central app device-twin' command group + 0.9.8 +++++++++++++++ General changes diff --git a/README.md b/README.md index de86b6ea3..7afbb269b 100644 --- a/README.md +++ b/README.md @@ -72,34 +72,25 @@ Commands: IoT Central ``` -$ az iot central app -h +$ az iot central -h Group - az iot central app : Manage Azure IoT Central applications. - - To use this command group, the user must be logged through the `az login` command, - have the correct tenant set (the users home tenant) and - have access to the application through http://apps.azureiotcentral.com". + az iot central : Manage IoT Central resources. + IoT Central is an IoT application platform that reduces the burden and cost of developing, + managing, and maintaining enterprise-grade IoT solutions. Choosing to build with IoT Central + gives you the opportunity to focus time, money, and energy on transforming your business + with IoT data, rather than just maintaining and updating a complex and continually evolving + IoT infrastructure. + IoT Central documentation is available at https://aka.ms/iotcentral-documentation. Subgroups: - api-token [Preview] : Create and Manage API tokens . - device [Preview] : Manage and configure IoT Central devices. - device-template [Preview] : Manage and configure IoT Central device templates. - device-twin : Manage IoT Central device twins. - user [Preview] : Manage and configure IoT Central users. - -Commands: - create : Create an IoT Central application. - delete : Delete an IoT Central application. - list : List IoT Central applications. - monitor-events : Monitor device telemetry & messages sent to the IoT Hub for an - IoT Central app. - monitor-properties [Preview] : Monitor desired and reported properties sent to/from - the IoT Hub for an IoT Central app. - show : Get the details of an IoT Central application. - update : Update metadata for an IoT Central application. - validate-messages [Preview] : Validate messages sent to the IoT Hub for an IoT - Central app. - validate-properties [Preview] : Validate reported properties sent to IoT Central app. + api-token [Preview] : Create and Manage API tokens. + app : Manage IoT Central applications. + device [Preview] : Manage and configure IoT Central devices. + device-template [Preview] : Manage and configure IoT Central device templates. + diagnostics [Preview] : Perform application and device level diagnostics. + user [Preview] : Manage and configure IoT Central users. + +For more specific examples, use: az find "az iot central" ``` diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 5430440cc..9ee722317 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -12,39 +12,46 @@ def load_central_help(): "iot central" ] = """ type: group - short-summary: Manage Azure Central (IoT Central) solutions & infrastructure - """ + short-summary: Manage IoT Central resources. + long-summary: | + IoT Central is an IoT application platform that reduces the burden and cost of developing, + managing, and maintaining enterprise-grade IoT solutions. Choosing to build with IoT Central + gives you the opportunity to focus time, money, and energy on transforming your business + with IoT data, rather than just maintaining and updating a complex and continually evolving + IoT infrastructure. + + IoT Central documentation is available at https://aka.ms/iotcentral-documentation + For more information on command usage, see: aka.ms/azure-cli-iot-ext + """ helps[ "iot central app" ] = """ type: group - short-summary: | - Manage Azure IoT Central applications. - - To use this command group, the user must be logged through the `az login` command, - have the correct tenant set (the users home tenant) and - have access to the application through http://apps.azureiotcentral.com" + short-summary: Manage IoT Central applications. + long-summary: Create, delete, view, and update your IoT Central apps. """ _load_central_devices_help() _load_central_users_help() _load_central_api_token_help() _load_central_device_templates_help() - _load_central_device_twin_help() _load_central_monitors_help() + _load_central_command_help() + # TODO: Delete this by end of Dec 2020 + _load_central_deprecated_commands() def _load_central_devices_help(): helps[ - "iot central app device" + "iot central device" ] = """ type: group short-summary: Manage and configure IoT Central devices """ helps[ - "iot central app device create" + "iot central device create" ] = """ type: command short-summary: Create a device in IoT Central @@ -52,13 +59,13 @@ def _load_central_devices_help(): examples: - name: Create a device text: > - az iot central app device create + az iot central device create --app-id {appid} --device-id {deviceid} - name: Create a simulated device text: > - az iot central app device create + az iot central device create --app-id {appid} --device-id {deviceid} --instance-of {devicetemplateid} @@ -66,7 +73,7 @@ def _load_central_devices_help(): """ helps[ - "iot central app device show" + "iot central device show" ] = """ type: command short-summary: Get a device from IoT Central @@ -74,13 +81,13 @@ def _load_central_devices_help(): examples: - name: Get a device text: > - az iot central app device show + az iot central device show --app-id {appid} --device-id {deviceid} """ helps[ - "iot central app device delete" + "iot central device delete" ] = """ type: command short-summary: Delete a device from IoT Central @@ -88,13 +95,13 @@ def _load_central_devices_help(): examples: - name: Delete a device text: > - az iot central app device delete + az iot central device delete --app-id {appid} --device-id {deviceid} """ helps[ - "iot central app device show-credentials" + "iot central device show-credentials" ] = """ type: command short-summary: Get device credentials from IoT Central @@ -102,13 +109,13 @@ def _load_central_devices_help(): examples: - name: Get device credentials for a device text: > - az iot central app device show-credentials + az iot central device show-credentials --app-id {appid} --device-id {deviceid} """ helps[ - "iot central app device registration-info" + "iot central device registration-info" ] = """ type: command short-summary: Get registration info on device(s) from IoT Central @@ -119,25 +126,35 @@ def _load_central_devices_help(): examples: - name: Get registration info on specified device text: > - az iot central app device registration-info --app-id {appid} --device-id {deviceid} + az iot central device registration-info --app-id {appid} --device-id {deviceid} """ + +def _load_central_command_help(): helps[ - "iot central app device registration-summary" + "iot central device command" + ] = """ + type: group + short-summary: Run device commands. + """ + + helps[ + "iot central device command history" ] = """ type: command - short-summary: Provides a registration summary of all the devices in an app. - long-summary: | - Note: This command can take a significant amount of time to return - if your app contains a lot of devices + short-summary: Get most recent command-response request and response payload. examples: - - name: Registration summary + - name: Show command response text: > - az iot central app device registration-summary --app-id {appid} + az iot central device command history + --app-id {appid} + --device-id {deviceid} + --interface-id {interfaceid} + --command-name {commandname} """ helps[ - "iot central app device run-command" + "iot central device command run" ] = """ type: command short-summary: Run a command on a device and view associated response. Does NOT monitor property updates that the command may perform. @@ -149,7 +166,7 @@ def _load_central_devices_help(): examples: - name: Run command response text: > - az iot central app device run-command + az iot central device command run --app-id {appid} --device-id {deviceid} --interface-id {interfaceid} @@ -158,7 +175,7 @@ def _load_central_devices_help(): - name: Short Run command response text: > - az iot central app device run-command + az iot central device command run -n {appid} -d {deviceid} -i {interfaceid} @@ -166,39 +183,24 @@ def _load_central_devices_help(): -k {payload} """ - helps[ - "iot central app device show-command-history" - ] = """ - type: command - short-summary: Get most recent command-response request and response payload. - examples: - - name: Show command response - text: > - az iot central app device show-command-history - --app-id {appid} - --device-id {deviceid} - --interface-id {interfaceid} - --command-name {commandname} - """ - def _load_central_users_help(): helps[ - "iot central app user" + "iot central user" ] = """ type: group short-summary: Manage and configure IoT Central users """ helps[ - "iot central app user create" + "iot central user create" ] = """ type: command short-summary: Add a user to the application examples: - name: Add a user by email to the application text: > - az iot central app user create + az iot central user create --user-id {userId} --app-id {appId} --email {emailAddress} @@ -206,7 +208,7 @@ def _load_central_users_help(): - name: Add a service-principal to the application text: > - az iot central app user create + az iot central user create --user-id {userId} --app-id {appId} --tenant-id {tenantId} @@ -214,41 +216,41 @@ def _load_central_users_help(): --role operator """ helps[ - "iot central app user show" + "iot central user show" ] = """ type: command short-summary: Get the details of a user by ID examples: - name: Get details of user text: > - az iot central app user show + az iot central user show --app-id {appid} --user-id {userId} """ helps[ - "iot central app user delete" + "iot central user delete" ] = """ type: command short-summary: Delete a user from the application examples: - name: Delete a user text: > - az iot central app user delete + az iot central user delete --app-id {appid} --user-id {userId} """ helps[ - "iot central app user list" + "iot central user list" ] = """ type: command short-summary: Get list of users in an application examples: - name: List of users text: > - az iot central app user list + az iot central user list --app-id {appid} """ @@ -256,14 +258,14 @@ def _load_central_users_help(): def _load_central_api_token_help(): helps[ - "iot central app api-token" + "iot central api-token" ] = """ type: group - short-summary: Create and Manage API tokens . + short-summary: Create and Manage API tokens. """ helps[ - "iot central app api-token create" + "iot central api-token create" ] = """ type: command short-summary: Create a new API token in the application @@ -271,14 +273,14 @@ def _load_central_api_token_help(): examples: - name: Add new API token text: > - az iot central app api-token create + az iot central api-token create --token-id {tokenId} --app-id {appId} --role admin """ helps[ - "iot central app api-token show" + "iot central api-token show" ] = """ type: command short-summary: Get token meta data (e.g. role as a GUID, expiration) @@ -286,26 +288,26 @@ def _load_central_api_token_help(): examples: - name: Get API token text: > - az iot central app api-token show + az iot central api-token show --app-id {appid} --token-id {tokenId} """ helps[ - "iot central app api-token delete" + "iot central api-token delete" ] = """ type: command short-summary: Delete an API token from the application examples: - name: Delete an API token text: > - az iot central app api-token delete + az iot central api-token delete --app-id {appid} --token-id {tokenId} """ helps[ - "iot central app api-token list" + "iot central api-token list" ] = """ type: command short-summary: Get a list of all token meta data (e.g. Role as a GUID and expiration) @@ -313,7 +315,7 @@ def _load_central_api_token_help(): examples: - name: List of API tokens text: > - az iot central app api-token list + az iot central api-token list --app-id {appid} """ @@ -321,14 +323,14 @@ def _load_central_api_token_help(): def _load_central_device_templates_help(): helps[ - "iot central app device-template" + "iot central device-template" ] = """ type: group short-summary: Manage and configure IoT Central device templates """ helps[ - "iot central app device-template create" + "iot central device-template create" ] = """ type: command short-summary: Create a device template in IoT Central @@ -336,21 +338,21 @@ def _load_central_device_templates_help(): examples: - name: Create a device template with payload read from a file text: > - az iot central app device-template create + az iot central device-template create --app-id {appid} --content {pathtofile} --device-template-id {devicetemplateid} - name: Create a device template with payload read from raw json text: > - az iot central app device-template create + az iot central device-template create --app-id {appid} --content {json} --device-template-id {devicetemplateid} """ helps[ - "iot central app device-template show" + "iot central device-template show" ] = """ type: command short-summary: Get a device template from IoT Central @@ -358,13 +360,13 @@ def _load_central_device_templates_help(): examples: - name: Get a device template text: > - az iot central app device-template show + az iot central device-template show --app-id {appid} --device-template-id {devicetemplateid} """ helps[ - "iot central app device-template delete" + "iot central device-template delete" ] = """ type: command short-summary: Delete a device template from IoT Central @@ -374,31 +376,23 @@ def _load_central_device_templates_help(): examples: - name: Delete a device template from IoT Central text: > - az iot central app device-template delete + az iot central device-template delete --app-id {appid} --device-template-id {devicetemplateid} """ -def _load_central_device_twin_help(): - helps[ - "iot central app device-twin" - ] = """ - type: group - short-summary: Manage IoT Central device twins. - """ +def _load_central_monitors_help(): helps[ - "iot central app device-twin show" + "iot central diagnostics" ] = """ - type: command - short-summary: Get the device twin from IoT Hub. + type: group + short-summary: Perform application and device level diagnostics. """ - -def _load_central_monitors_help(): helps[ - "iot central app monitor-events" + "iot central diagnostics monitor-events" ] = """ type: command short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. @@ -409,38 +403,38 @@ def _load_central_monitors_help(): examples: - name: Basic usage text: > - az iot central app monitor-events --app-id {app_id} + az iot central diagnostics monitor-events --app-id {app_id} - name: Basic usage when filtering on target device text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} + az iot central diagnostics monitor-events --app-id {app_id} -d {device_id} - name: Basic usage when filtering targeted devices with a wildcard in the ID text: > - az iot central app monitor-events --app-id {app_id} -d Device*d + az iot central diagnostics monitor-events --app-id {app_id} -d Device*d - name: Basic usage when filtering on module. text: > - az iot central app monitor-events --app-id {app_id} -m {module_id} + az iot central diagnostics monitor-events --app-id {app_id} -m {module_id} - name: Basic usage when filtering targeted modules with a wildcard in the ID text: > - az iot central app monitor-events --app-id {app_id} -m Module* + az iot central diagnostics monitor-events --app-id {app_id} -m Module* - name: Filter device and specify an Event Hub consumer group to bind to. text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} + az iot central diagnostics monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} - name: Receive message annotations (message headers) text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno + az iot central diagnostics monitor-events --app-id {app_id} -d {device_id} --properties anno - name: Receive message annotations + system properties. Never time out. text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno sys --timeout 0 + az iot central diagnostics monitor-events --app-id {app_id} -d {device_id} --properties anno sys --timeout 0 - name: Receive all message attributes from all device messages text: > - az iot central app monitor-events --app-id {app_id} --props all + az iot central diagnostics monitor-events --app-id {app_id} --props all - name: Receive all messages and parse message payload as JSON text: > - az iot central app monitor-events --app-id {app_id} --output json + az iot central diagnostics monitor-events --app-id {app_id} --output json """ helps[ - "iot central app validate-messages" + "iot central diagnostics validate-messages" ] = """ type: command short-summary: Validate messages sent to the IoT Hub for an IoT Central app. @@ -451,29 +445,29 @@ def _load_central_monitors_help(): examples: - name: Basic usage text: > - az iot central app validate-messages --app-id {app_id} + az iot central diagnostics validate-messages --app-id {app_id} - name: Output errors as they are detected text: > - az iot central app validate-messages --app-id {app_id} --style scroll + az iot central diagnostics validate-messages --app-id {app_id} --style scroll - name: Basic usage when filtering on target device text: > - az iot central app validate-messages --app-id {app_id} -d {device_id} + az iot central diagnostics validate-messages --app-id {app_id} -d {device_id} - name: Basic usage when filtering targeted devices with a wildcard in the ID text: > - az iot central app validate-messages --app-id {app_id} -d Device* + az iot central diagnostics validate-messages --app-id {app_id} -d Device* - name: Basic usage when filtering on module. text: > - az iot central app validate-messages --app-id {app_id} -m {module_id} + az iot central diagnostics validate-messages --app-id {app_id} -m {module_id} - name: Basic usage when filtering targeted modules with a wildcard in the ID text: > - az iot central app validate-messages --app-id {app_id} -m Module* + az iot central diagnostics validate-messages --app-id {app_id} -m Module* - name: Filter device and specify an Event Hub consumer group to bind to. text: > - az iot central app validate-messages --app-id {app_id} -d {device_id} --cg {consumer_group_name} + az iot central diagnostics validate-messages --app-id {app_id} -d {device_id} --cg {consumer_group_name} """ helps[ - "iot central app monitor-properties" + "iot central diagnostics monitor-properties" ] = """ type: command short-summary: Monitor desired and reported properties sent to/from the IoT Hub for an IoT Central app. @@ -484,11 +478,11 @@ def _load_central_monitors_help(): examples: - name: Basic usage text: > - az iot central app monitor-properties --app-id {app_id} -d {device_id} + az iot central diagnostics monitor-properties --app-id {app_id} -d {device_id} """ helps[ - "iot central app validate-properties" + "iot central diagnostics validate-properties" ] = """ type: command short-summary: Validate reported properties sent to IoT Central app. @@ -500,5 +494,92 @@ def _load_central_monitors_help(): examples: - name: Basic usage text: > - az iot central app validate-properties --app-id {app_id} -d {device_id} + az iot central diagnostics validate-properties --app-id {app_id} -d {device_id} + """ + + helps[ + "iot central diagnostics registration-summary" + ] = """ + type: command + short-summary: Provides a registration summary of all the devices in an app. + long-summary: | + Note: This command can take a significant amount of time to return + if your app contains a lot of devices + examples: + - name: Registration summary + text: > + az iot central diagnostics registration-summary --app-id {appid} + """ + + +# TODO: Delete this by end of Dec 2020 +def _load_central_deprecated_commands(): + helps[ + "iot central app device-twin" + ] = """ + type: group + short-summary: Manage IoT Central device twins. + """ + + helps[ + "iot central app device-twin show" + ] = """ + type: command + short-summary: Get the device twin from IoT Hub. + """ + + helps[ + "iot central device twin" + ] = """ + type: group + short-summary: Manage IoT Central device twins. + """ + + helps[ + "iot central device twin show" + ] = """ + type: command + short-summary: Get the device twin from IoT Hub. + """ + + helps[ + "iot central app monitor-events" + ] = """ + type: command + short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. + long-summary: | + EXPERIMENTAL requires Python 3.5+ + This command relies on and may install dependent Cython package (uamqp) upon first execution. + https://github.com/Azure/azure-uamqp-python + examples: + - name: Basic usage + text: > + az iot central app monitor-events --app-id {app_id} + - name: Basic usage when filtering on target device + text: > + az iot central app monitor-events --app-id {app_id} -d {device_id} + - name: Basic usage when filtering targeted devices with a wildcard in the ID + text: > + az iot central app monitor-events --app-id {app_id} -d Device*d + - name: Basic usage when filtering on module. + text: > + az iot central app monitor-events --app-id {app_id} -m {module_id} + - name: Basic usage when filtering targeted modules with a wildcard in the ID + text: > + az iot central app monitor-events --app-id {app_id} -m Module* + - name: Filter device and specify an Event Hub consumer group to bind to. + text: > + az iot central app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} + - name: Receive message annotations (message headers) + text: > + az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno + - name: Receive message annotations + system properties. Never time out. + text: > + az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno sys --timeout 0 + - name: Receive all message attributes from all device messages + text: > + az iot central app monitor-events --app-id {app_id} --props all + - name: Receive all messages and parse message payload as JSON + text: > + az iot central app monitor-events --app-id {app_id} --output json """ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index f1c860342..90ff0a083 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -37,16 +37,30 @@ def load_central_commands(self, _): """ Load CLI commands """ + with self.command_group( - "iot central app", command_type=central_monitor_ops, + "iot central diagnostics", command_type=central_monitor_ops, is_preview=True ) as cmd_group: cmd_group.command("monitor-events", "monitor_events") - cmd_group.command("validate-messages", "validate_messages", is_preview=True) - cmd_group.command("monitor-properties", "monitor_properties", is_preview=True) - cmd_group.command("validate-properties", "validate_properties", is_preview=True) + cmd_group.command( + "validate-messages", "validate_messages", + ) + cmd_group.command( + "monitor-properties", "monitor_properties", + ) + cmd_group.command( + "validate-properties", "validate_properties", + ) + + with self.command_group( + "iot central diagnostics", command_type=central_device_ops, is_preview=True + ) as cmd_group: + cmd_group.command( + "registration-summary", "registration_summary", + ) with self.command_group( - "iot central app user", command_type=central_user_ops, is_preview=True, + "iot central user", command_type=central_user_ops, is_preview=True, ) as cmd_group: cmd_group.command("create", "add_user") cmd_group.command("list", "list_users") @@ -54,9 +68,7 @@ def load_central_commands(self, _): cmd_group.command("delete", "delete_user") with self.command_group( - "iot central app api-token", - command_type=central_api_token_ops, - is_preview=True, + "iot central api-token", command_type=central_api_token_ops, is_preview=True, ) as cmd_group: cmd_group.command("create", "add_api_token") cmd_group.command("list", "list_api_tokens") @@ -64,22 +76,23 @@ def load_central_commands(self, _): cmd_group.command("delete", "delete_api_token") with self.command_group( - "iot central app device", command_type=central_device_ops, is_preview=True, + "iot central device", command_type=central_device_ops, is_preview=True, ) as cmd_group: # cmd_group.command("list", "list_devices") cmd_group.command("show", "get_device") cmd_group.command("create", "create_device") cmd_group.command("delete", "delete_device") - cmd_group.command("run-command", "run_command") - cmd_group.command("show-command-history", "get_command_history") cmd_group.command("registration-info", "registration_info") - cmd_group.command( - "registration-summary", "registration_summary", - ) cmd_group.command("show-credentials", "get_credentials") with self.command_group( - "iot central app device-template", + "iot central device command", command_type=central_device_ops, is_preview=True, + ) 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, ) as cmd_group: @@ -90,6 +103,33 @@ def load_central_commands(self, _): cmd_group.command("delete", "delete_device_template") with self.command_group( - "iot central app device-twin", command_type=central_device_twin_ops + "iot central device twin", command_type=central_device_twin_ops, is_preview=True + ) as cmd_group: + cmd_group.command( + "show", "device_twin_show", + ) + + # TODO: Delete this by end of Dec 2020 + _load_central_deprecated_commands(self, _) + + +def _load_central_deprecated_commands(self, _): + with self.command_group( + "iot central app device-twin", + command_type=central_device_twin_ops, + deprecate_info=self.deprecate(redirect="iot central device twin"), ) as cmd_group: - cmd_group.command("show", "device_twin_show") + cmd_group.command( + "show", "device_twin_show", + ) + + with self.command_group( + "iot central app", command_type=central_monitor_ops, + ) as cmd_group: + cmd_group.command( + "monitor-events", + "monitor_events", + deprecate_info=self.deprecate( + redirect="iot central diagnostics monitor-events" + ), + ) diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 736aa016b..0d465db69 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -30,7 +30,7 @@ options_list=["--style"], choices=CaseInsensitiveList(["scroll", "json", "csv"]), help="Indicate output style" - "scroll = deliver errors as they arrive, json = summarize results as json, csv = summarize results as json", + "scroll = deliver errors as they arrive, json = summarize results as json, csv = summarize results as csv", ) @@ -38,15 +38,59 @@ def load_central_arguments(self, _): """ Load CLI Args for Knack parser """ - with self.argument_context("iot central app") as context: + with self.argument_context("iot central") as context: context.argument("app_id", options_list=["--app-id", "-n"], help="Target App.") - context.argument("minimum_severity", arg_type=severity_type) + context.argument( + "token", + options_list=["--token"], + help="Authorization token for request. " + "More info available here: https://docs.microsoft.com/en-us/learn/modules/manage-iot-central-apps-with-rest-api/ " + "MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...'). " + "Example: 'Bearer someBearerTokenHere'", + ) + context.argument( + "central_dns_suffix", + options_list=["--central-dns-suffix", "--central-api-uri"], + help="Central dns suffix. " + "This enables running cli commands against non public/prod environments", + ) + + with self.argument_context("iot central device-template") as context: + context.argument( + "device_template_id", + options_list=["--device-template-id", "--dtid"], + help="Device template id. Example: somedevicetemplate", + ) + context.argument( + "content", + options_list=["--content", "-k"], + help="Configuration for request. " + "Provide path to JSON file or raw stringified JSON. " + "[File Path Example: ./path/to/file.json] " + "[Stringified JSON Example: {'a': 'b'}] ", + ) + + with self.argument_context("iot central api-token") as context: + context.argument( + "token_id", + options_list=["--token-id", "--tkid"], + help="Unique ID for the API token. ", + ) context.argument("role", arg_type=role_type) + + 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", ) + 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", + ) context.argument( "device_name", options_list=["--device-name"], @@ -62,18 +106,6 @@ def load_central_arguments(self, _): options_list=["--command-name", "--cn"], help="Command name as specified in device template. Example: run_firmware_update", ) - 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", - ) - context.argument( - "device_template_id", - options_list=["--device-template-id", "--dtid"], - help="Device template id. Example: somedevicetemplate", - ) context.argument( "content", options_list=["--content", "-k"], @@ -82,32 +114,12 @@ def load_central_arguments(self, _): "[File Path Example: ./path/to/file.json] " "[Stringified JSON Example: {'a': 'b'}] ", ) + + with self.argument_context("iot central user") as context: context.argument( - "token", - options_list=["--token"], - help="Authorization token for request. " - "More info available here: https://docs.microsoft.com/en-us/learn/modules/manage-iot-central-apps-with-rest-api/ " - "MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...'). " - "Example: 'Bearer someBearerTokenHere'", - ) - context.argument( - "central_dns_suffix", - options_list=["--central-dns-suffix", "--central-api-uri"], - help="Central dns suffix. " - "This enables running cli commands against non public/prod environments", - ) - context.argument( - "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", - ) - context.argument( - "assignee", - options_list=["--user-id", "--assignee"], - help="ID associated with the user. ", - ) - context.argument( - "email", - options_list=["--email"], - help="Email address of user to be added to the app. ", + "tenant_id", + options_list=["--tenant-id", "--tnid"], + help="Tenant ID for service principal to be added to the app. Object ID must also be specified. ", ) context.argument( "object_id", @@ -115,24 +127,24 @@ def load_central_arguments(self, _): help="Object ID for service principal to be added to the app. Tenant ID must also be specified. ", ) context.argument( - "tenant_id", - options_list=["--tenant-id", "--tnid"], - help="Tenant ID for service principal to be added to the app. Object ID must also be specified. ", + "email", + options_list=["--email"], + help="Email address of user to be added to the app. ", ) - context.argument( - "token_id", - options_list=["--token-id", "--tkid"], - help="Unique ID for the API token. ", + "assignee", + options_list=["--user-id", "--assignee"], + help="ID associated with the user. ", ) + context.argument("role", arg_type=role_type) - with self.argument_context("iot central app monitor-events") as context: - context.argument("timeout", arg_type=event_timeout_type) - context.argument("properties", arg_type=event_msg_prop_type) - - with self.argument_context("iot central app validate-messages") as context: + with self.argument_context("iot central diagnostics") as context: context.argument("timeout", arg_type=event_timeout_type) context.argument("properties", arg_type=event_msg_prop_type) + context.argument( + "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", + ) + context.argument("minimum_severity", arg_type=severity_type) context.argument("style", arg_type=style_type) context.argument( "duration", @@ -148,3 +160,18 @@ def load_central_arguments(self, _): help="Maximum number of messages to recieve from target device before terminating connection." "Use 0 for infinity.", ) + context.argument( + "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", + ) + # TODO: Delete this by end of Dec 2020 + load_deprecated_params(self, _) + + +def load_deprecated_params(self, _): + with self.argument_context("iot central app monitor-events") as context: + context.argument("timeout", arg_type=event_timeout_type) + context.argument("properties", arg_type=event_msg_prop_type) + context.argument( + "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", + ) + context.argument("minimum_severity", arg_type=severity_type) diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index e269efc52..fc8160d55 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -51,6 +51,22 @@ def test_central_device_twin_show_fail(self): expect_failure=True, ) + # Verify incorrect app-id throws error + self.cmd( + "iot central device twin show --app-id incorrect-app --device-id {}".format( + device_id + ), + expect_failure=True, + ) + + # Verify incorrect device-id throws error + self.cmd( + "iot central device twin show --app-id {} --device-id incorrect-device".format( + APP_ID + ), + expect_failure=True, + ) + self._delete_device(device_id) def test_central_device_twin_show_success(self): @@ -67,10 +83,18 @@ def test_central_device_twin_show_success(self): checks=[self.check("deviceId", device_id)], ) + self.cmd( + "iot central device twin show --app-id {} --device-id {}".format( + APP_ID, device_id + ), + checks=[self.check("deviceId", device_id)], + ) + self._delete_device(device_id) self._delete_device_template(template_id) - def test_central_monitor_events(self): + # TODO: Delete this by end of Dec 2020 + def test_central_monitor_events_deprecated(self): (template_id, _) = self._create_device_template() (device_id, _) = self._create_device(instance_of=template_id) credentials = self._get_credentials(device_id) @@ -93,6 +117,39 @@ def test_central_monitor_events(self): expect_failure=True, ) + # Ensure no failure + output = self._get_monitor_events_output_deprecated(device_id, enqueued_time) + + self._delete_device(device_id) + self._delete_device_template(template_id) + assert '"Bool": true' in output + assert device_id in output + + def test_central_monitor_events(self): + (template_id, _) = self._create_device_template() + (device_id, _) = self._create_device(instance_of=template_id) + credentials = self._get_credentials(device_id) + + device_client = helpers.dps_connect_device(device_id, credentials) + + payload = {"Bool": True} + msg = Message( + data=json.dumps(payload), + content_encoding="utf-8", + content_type="application/json", + ) + device_client.send_message(msg) + + enqueued_time = utility.calculate_millisec_since_unix_epoch_utc() - 10000 + + # Test with invalid app-id + self.cmd( + "iot central diagnostics monitor-events --app-id {} -y".format( + APP_ID + "zzz" + ), + expect_failure=True, + ) + # Ensure no failure output = self._get_monitor_events_output(device_id, enqueued_time) @@ -209,7 +266,7 @@ def test_central_device_methods_CRD(self): (device_id, device_name) = self._create_device() self.cmd( - "iot central app device show --app-id {} -d {}".format(APP_ID, device_id), + "iot central device show --app-id {} -d {}".format(APP_ID, device_id), checks=[ self.check("approved", True), self.check("displayName", device_name), @@ -224,13 +281,13 @@ def test_central_user_methods_CRD(self): users = self._create_users() self.cmd( - "iot central app user show --app-id {} --user-id {}".format( + "iot central user show --app-id {} --user-id {}".format( APP_ID, users[0].get("id") ), ) result = self.cmd( - "iot central app user list --app-id {}".format(APP_ID,), + "iot central user list --app-id {}".format(APP_ID,), ).get_output_in_json() user_list = result.get("value") @@ -245,13 +302,13 @@ def test_central_api_token_methods_CRD(self): tokens = self._create_api_tokens() self.cmd( - "iot central app api-token show --app-id {} --token-id {}".format( + "iot central api-token show --app-id {} --token-id {}".format( APP_ID, tokens[0].get("id") ), ) result = self.cmd( - "iot central app api-token list --app-id {}".format(APP_ID,), + "iot central api-token list --app-id {}".format(APP_ID,), ).get_output_in_json() token_list = result.get("value") @@ -272,7 +329,7 @@ def test_central_device_template_methods_CRD(self): (template_id, template_name) = self._create_device_template() self.cmd( - "iot central app device-template show --app-id {} --device-template-id {}".format( + "iot central device-template show --app-id {} --device-template-id {}".format( APP_ID, template_id ), checks=[ @@ -290,7 +347,7 @@ def test_central_device_registration_info_registered(self): ) result = self.cmd( - "iot central app device registration-info --app-id {} -d {}".format( + "iot central device registration-info --app-id {} -d {}".format( APP_ID, device_id ) ) @@ -331,7 +388,7 @@ def test_central_run_command(self): self._wait_for_provisioned(device_id) run_command_result = self.cmd( - "iot central app device run-command" + "iot central device command run" " -n {}" " -d {}" " -i {}" @@ -343,7 +400,7 @@ def test_central_run_command(self): ) show_command_result = self.cmd( - "iot central app device show-command-history" + "iot central device command history" " -n {}" " -d {}" " -i {}" @@ -368,7 +425,7 @@ def test_central_device_registration_info_unassociated(self): (device_id, device_name) = self._create_device() result = self.cmd( - "iot central app device registration-info --app-id {} -d {}".format( + "iot central device registration-info --app-id {} -d {}".format( APP_ID, device_id ) ) @@ -405,7 +462,7 @@ def test_central_device_registration_info_unassociated(self): def test_central_device_registration_summary(self): result = self.cmd( - "iot central app device registration-summary --app-id {}".format(APP_ID) + "iot central diagnostics registration-summary --app-id {}".format(APP_ID) ) json_result = result.get_output_in_json() @@ -424,7 +481,7 @@ def _create_device(self, **kwargs) -> (str, str): device_id = self.create_random_name(prefix="aztest", length=24) device_name = self.create_random_name(prefix="aztest", length=24) - command = "iot central app device create --app-id {} -d {} --device-name {}".format( + command = "iot central device create --app-id {} -d {} --device-name {}".format( APP_ID, device_id, device_name ) checks = [ @@ -453,7 +510,7 @@ def _create_users(self,): for role in Role: user_id = self.create_random_name(prefix="aztest", length=24) email = user_id + "@microsoft.com" - command = "iot central app user create --app-id {} --user-id {} -r {} --email {}".format( + command = "iot central user create --app-id {} --user-id {} -r {} --email {}".format( APP_ID, user_id, role.name, email, ) @@ -469,9 +526,7 @@ def _create_users(self,): def _delete_user(self, user_id) -> None: self.cmd( - "iot central app user delete --app-id {} --user-id {}".format( - APP_ID, user_id - ), + "iot central user delete --app-id {} --user-id {}".format(APP_ID, user_id), checks=[self.check("result", "success")], ) @@ -480,7 +535,7 @@ def _create_api_tokens(self,): tokens = [] for role in Role: token_id = self.create_random_name(prefix="aztest", length=24) - command = "iot central app api-token create --app-id {} --token-id {} -r {}".format( + command = "iot central api-token create --app-id {} --token-id {} -r {}".format( APP_ID, token_id, role.name, ) @@ -494,16 +549,14 @@ def _create_api_tokens(self,): def _delete_api_token(self, token_id) -> None: self.cmd( - "iot central app api-token delete --app-id {} --token-id {}".format( + "iot central api-token delete --app-id {} --token-id {}".format( APP_ID, token_id ), checks=[self.check("result", "success")], ) def _wait_for_provisioned(self, device_id): - command = "iot central app device show --app-id {} -d {}".format( - APP_ID, device_id - ) + command = "iot central device show --app-id {} -d {}".format(APP_ID, device_id) while True: result = self.cmd(command) device = result.get_output_in_json() @@ -517,7 +570,7 @@ def _wait_for_provisioned(self, device_id): def _delete_device(self, device_id) -> None: self.cmd( - "iot central app device delete --app-id {} -d {}".format(APP_ID, device_id), + "iot central device delete --app-id {} -d {}".format(APP_ID, device_id), checks=[self.check("result", "success")], ) @@ -529,7 +582,7 @@ def _create_device_template(self): template_id = template_name + "id" self.cmd( - "iot central app device-template create --app-id {} --device-template-id {} -k '{}'".format( + "iot central device-template create --app-id {} --device-template-id {} -k '{}'".format( APP_ID, template_id, device_template_path ), checks=[ @@ -542,7 +595,7 @@ def _create_device_template(self): def _delete_device_template(self, template_id): attempts = range(0, 10) - command = "iot central app device-template delete --app-id {} --device-template-id {}".format( + command = "iot central device-template delete --app-id {} --device-template-id {}".format( APP_ID, template_id ) @@ -556,7 +609,7 @@ def _delete_device_template(self, template_id): def _get_credentials(self, device_id): return self.cmd( - "iot central app device show-credentials --app-id {} -d {}".format( + "iot central device show-credentials --app-id {} -d {}".format( APP_ID, device_id ) ).get_output_in_json() @@ -568,7 +621,7 @@ def _get_validate_messages_output( asserts = [] output = self.command_execute_assert( - "iot central app validate-messages --app-id {} -d {} --et {} --duration {} --mm {} -y --style json".format( + "iot central diagnostics validate-messages --app-id {} -d {} --et {} --duration {} --mm {} -y --style json".format( APP_ID, device_id, enqueued_time, duration, max_messages ), asserts, @@ -583,6 +636,25 @@ def _get_monitor_events_output(self, device_id, enqueued_time, asserts=None): if not asserts: asserts = [] + output = self.command_execute_assert( + "iot central diagnostics monitor-events -n {} -d {} --et {} --to 1 -y".format( + APP_ID, device_id, enqueued_time + ), + asserts, + ) + + if not output: + output = "" + + return output + + # TODO: Delete this by end of Dec 2020 + def _get_monitor_events_output_deprecated( + self, device_id, enqueued_time, asserts=None + ): + if not asserts: + asserts = [] + output = self.command_execute_assert( "iot central app monitor-events -n {} -d {} --et {} --to 1 -y".format( APP_ID, device_id, enqueued_time From ed7a7ca4f1327c32ea6a9ca02501055ecd814115 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Thu, 20 Aug 2020 10:48:03 -0700 Subject: [PATCH 099/179] C2D enhancements (#238) * Implement c2d-message purge command with tests * Added --ack argument to c2d-message receive to ack message after retrieval * Flake8 fixes * Rework of message ack argument command into separate arguments, added help example text and updated tests * Switched print statement to logger.debug * History updates for c2d message enhancements --- HISTORY.rst | 6 + azext_iot/_help.py | 22 ++ azext_iot/_params.py | 45 +++- azext_iot/commands.py | 1 + azext_iot/operations/hub.py | 73 +++++- azext_iot/tests/test_iot_ext_unit.py | 271 +++++++++++++++++++--- azext_iot/tests/test_iot_messaging_int.py | 49 ++++ 7 files changed, 421 insertions(+), 46 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5d3d55930..ae900f172 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,12 @@ IoT Central updates * 'az iot central app device show-command-history' moved to 'az iot central device command history' * 'az iot central device twin' added to support deprecation of 'az iot central app device-twin' command group +Cloud-to-Device message enhancements +* Introduced new `az iot device c2d-message purge` command to purge the message queue for a device. +* Added message ack arguments to `az iot c2d-message receive` to ack the message after it is received: + * Options are `--complete`, `--abandon`, and `--reject`, and only one can be used per command. + * `az iot device c2d-message receive` with no ack arguments remains unchanged and will not ack the message. + 0.9.8 +++++++++++++++ General changes diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 85bbcab93..bf751f685 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -727,6 +727,21 @@ ] = """ type: command short-summary: Receive a cloud-to-device message. + long-summary: | + Note: Only one message ack argument [--complete, --reject, --abandon] will be accepted. + examples: + - name: Basic usage + text: > + az iot device c2d-message receive -d {device_id} -n {hub_name} -g {resource_group} + - name: Receive a message and set a lock timeout of 30 seconds for that message + text: > + az iot device c2d-message receive -d {device_id} -n {hub_name} -g {resource_group} --lt {30} + - name: Receive a message and ack it as 'complete' after it is received + text: > + az iot device c2d-message receive -d {device_id} -n {hub_name} -g {resource_group} --complete + - name: Receive a message and reject it after it is received + text: > + az iot device c2d-message receive -d {device_id} -n {hub_name} -g {resource_group} --reject """ helps[ @@ -736,6 +751,13 @@ short-summary: Reject or deadletter a cloud-to-device message. """ +helps[ + "iot device c2d-message purge" +] = """ + type: command + short-summary: Purge cloud-to-device message queue for a target device. +""" + helps[ "iot device c2d-message send" ] = """ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 7397c358c..60cbe1c8e 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -551,17 +551,6 @@ def load_arguments(self, _): ) with self.argument_context("iot device c2d-message") as context: - context.argument( - "ack", - options_list=["--ack"], - arg_type=get_enum_type(AckType), - help="Request the delivery of per-message feedback regarding the final state of that message. " - "The description of ack values is as follows. " - "Positive: If the c2d message reaches the Completed state, IoT Hub generates a feedback message. " - "Negative: If the c2d message reaches the Dead lettered state, IoT Hub generates a feedback message. " - "Full: IoT Hub generates a feedback message in either case. " - "By default, no ack is requested.", - ) context.argument( "correlation_id", options_list=["--correlation-id", "--cid"], @@ -607,6 +596,17 @@ def load_arguments(self, _): ) with self.argument_context("iot device c2d-message send") as context: + context.argument( + "ack", + options_list=["--ack"], + arg_type=get_enum_type(AckType), + help="Request the delivery of per-message feedback regarding the final state of that message. " + "The description of ack values is as follows. " + "Positive: If the c2d message reaches the Completed state, IoT Hub generates a feedback message. " + "Negative: If the c2d message reaches the Dead lettered state, IoT Hub generates a feedback message. " + "Full: IoT Hub generates a feedback message in either case. " + "By default, no ack is requested.", + ) context.argument( "wait_on_feedback", options_list=["--wait", "-w"], @@ -614,6 +614,29 @@ def load_arguments(self, _): help="If set the c2d send operation will block until device feedback has been received.", ) + with self.argument_context("iot device c2d-message receive") as context: + context.argument( + "abandon", + arg_group="Message Ack", + options_list=["--abandon"], + arg_type=get_three_state_flag(), + help="Abandon the cloud-to-device message after receipt.", + ) + context.argument( + "complete", + arg_group="Message Ack", + options_list=["--complete"], + arg_type=get_three_state_flag(), + help="Complete the cloud-to-device message after receipt.", + ) + context.argument( + "reject", + arg_group="Message Ack", + options_list=["--reject"], + arg_type=get_three_state_flag(), + help="Reject the cloud-to-device message after receipt.", + ) + with self.argument_context("iot device upload-file") as context: context.argument( "file_path", diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 328923be0..c1071de43 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -160,6 +160,7 @@ def load_command_table(self, _): cmd_group.command("reject", "iot_c2d_message_reject") cmd_group.command("receive", "iot_c2d_message_receive") cmd_group.command("send", "iot_c2d_message_send") + cmd_group.command("purge", "iot_c2d_message_purge") with self.command_group("iot dps enrollment", command_type=iotdps_ops) as cmd_group: cmd_group.command("create", "iot_dps_device_enrollment_create") diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 9e2255b51..671b1d1d2 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -23,6 +23,7 @@ ProtocolType, ConfigType, KeyType, + SettleType, ) from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot.common.utility import ( @@ -1838,16 +1839,38 @@ def _iot_c2d_message_abandon(target, device_id, etag): def iot_c2d_message_receive( - cmd, device_id, hub_name=None, lock_timeout=60, resource_group_name=None, login=None + cmd, + device_id, + hub_name=None, + lock_timeout=60, + resource_group_name=None, + login=None, + abandon=None, + complete=None, + reject=None ): + ack = None + ack_vals = [abandon, complete, reject] + if any(ack_vals): + if len(list(filter(lambda val: val, ack_vals))) > 1: + raise CLIError( + "Only one c2d-message ack argument can be used [--complete, --abandon, --reject]" + ) + if abandon: + ack = SettleType.abandon.value + elif complete: + ack = SettleType.complete.value + elif reject: + ack = SettleType.reject.value + discovery = IotHubDiscovery(cmd) target = discovery.get_target( hub_name=hub_name, resource_group_name=resource_group_name, login=login ) - return _iot_c2d_message_receive(target, device_id, lock_timeout) + return _iot_c2d_message_receive(target, device_id, lock_timeout, ack) -def _iot_c2d_message_receive(target, device_id, lock_timeout=60): +def _iot_c2d_message_receive(target, device_id, lock_timeout=60, ack=None): from azext_iot.constants import MESSAGING_HTTP_C2D_SYSTEM_PROPERTIES resolver = SdkResolver(target=target, device_id=device_id) @@ -1866,7 +1889,32 @@ def _iot_c2d_message_receive(target, device_id, lock_timeout=60): payload = {"properties": {}} if "etag" in result.headers: - payload["etag"] = result.headers["etag"].strip('"') + eTag = result.headers["etag"].strip('"') + payload["etag"] = eTag + + if ack: + 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 + ) + 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 + ) + else: + logger.debug("__Completing message__") + ack_response = device_sdk.device.complete_device_bound_notification( + id=device_id, etag=eTag, raw=True + ) + + payload["ack"] = ( + ack + if (ack_response and ack_response.response.status_code == 204) + else None + ) app_prop_prefix = "iothub-app-" app_prop_keys = [ @@ -2069,6 +2117,19 @@ def http_wrap(target, device_id, generator): token.set() +def iot_c2d_message_purge( + 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 + ) + resolver = SdkResolver(target=target) + service_sdk = resolver.get_sdk(SdkType.service_sdk) + + return service_sdk.registry_manager.purge_command_queue(device_id) + + def _iot_simulate_get_default_properties(protocol): default_properties = {} is_mqtt = protocol == ProtocolType.mqtt.name @@ -2079,8 +2140,8 @@ def _iot_simulate_get_default_properties(protocol): return default_properties -def _handle_c2d_msg(target, device_id, receive_settle): - result = _iot_c2d_message_receive(target, device_id) +def _handle_c2d_msg(target, device_id, receive_settle, lock_timeout=60): + result = _iot_c2d_message_receive(target, device_id, lock_timeout) if result: six.print_() six.print_("__Received C2D Message__") diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index cb4782079..5b6abcd64 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -42,6 +42,12 @@ child_device_id = "child_device1" module_id = "mymod" config_id = "myconfig" +message_etag = "3k28zb44-0d00-4ddd-ade3-6110eb94c476" +c2d_purge_response = { + "deviceId": device_id, + "moduleId": None, + "totalMessagesPurged": 3 +} generic_cs_template = "HostName={};SharedAccessKeyName={};SharedAccessKey={}" @@ -1437,7 +1443,7 @@ def test_device_method_error(self, serviceclient_generic_error): class TestCloudToDeviceMessaging: @pytest.fixture(params=["full", "min"]) def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): - from . generators import create_c2d_receive_response + from .generators import create_c2d_receive_response if request.param == "full": payload = create_c2d_receive_response() @@ -1446,7 +1452,11 @@ def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): mocked_response.add( method=responses.GET, - url=re.compile("https://{}/devices/(.+)/messages/deviceBound".format(mock_target["entity"])), + url=re.compile( + "https://{}/devices/{}/messages/deviceBound".format( + mock_target["entity"], device_id + ) + ), body=payload["body"], headers=payload["headers"], status=200, @@ -1455,6 +1465,103 @@ def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): yield (mocked_response, payload) + @pytest.fixture() + def c2d_receive_ack_scenario(self, fixture_ghcs, mocked_response): + from .generators import create_c2d_receive_response + payload = create_c2d_receive_response() + mocked_response.add( + method=responses.GET, + url=( + "https://{}/devices/{}/messages/deviceBound".format( + mock_target["entity"], device_id + ) + ), + body=payload["body"], + headers=payload["headers"], + status=200, + match_querystring=False, + ) + + eTag = payload["headers"]["etag"].strip('"') + # complete / reject + mocked_response.add( + method=responses.DELETE, + url="https://{}/devices/{}/messages/deviceBound/{}".format( + mock_target["entity"], device_id, eTag + ), + body="", + headers=payload["headers"], + status=204, + match_querystring=False, + ) + + # abandon + mocked_response.add( + method=responses.POST, + url="https://{}/devices/{}/messages/deviceBound/{}/abandon".format( + mock_target["entity"], device_id, eTag + ), + body="", + headers=payload["headers"], + status=204, + match_querystring=False, + ) + + yield (mocked_response, payload) + + @pytest.fixture() + def c2d_ack_complete_scenario(self, fixture_ghcs, mocked_response): + mocked_response.add( + method=responses.DELETE, + url="https://{}/devices/{}/messages/deviceBound/{}".format( + mock_target["entity"], device_id, message_etag + ), + body="", + status=204, + match_querystring=False, + ) + yield mocked_response + + @pytest.fixture() + def c2d_ack_reject_scenario(self, fixture_ghcs, mocked_response): + mocked_response.add( + method=responses.DELETE, + url="https://{}/devices/{}/messages/deviceBound/{}?reject=".format( + mock_target["entity"], device_id, message_etag + ), + body="", + status=204, + match_querystring=False, + ) + yield mocked_response + + @pytest.fixture() + def c2d_ack_abandon_scenario(self, fixture_ghcs, mocked_response): + mocked_response.add( + method=responses.POST, + url="https://{}/devices/{}/messages/deviceBound/{}/abandon".format( + mock_target["entity"], device_id, message_etag + ), + body="", + status=204, + match_querystring=False, + ) + yield mocked_response + + @pytest.fixture() + def c2d_purge_scenario(self, fixture_ghcs, mocked_response): + import json + mocked_response.add( + method=responses.DELETE, + url="https://{}/devices/{}/commands".format( + mock_target["entity"], device_id + ), + body=json.dumps(c2d_purge_response), + content_type='application/json', + status=200, + ) + yield mocked_response + def test_c2d_receive(self, c2d_receive_scenario): service_client = c2d_receive_scenario[0] sample_c2d_receive = c2d_receive_scenario[1] @@ -1490,69 +1597,160 @@ def test_c2d_receive(self, c2d_receive_scenario): if sample_c2d_receive.get("body"): assert result["data"] == sample_c2d_receive["body"] - def test_c2d_complete(self, fixture_service_client_generic): - service_client = fixture_service_client_generic + def test_c2d_receive_ack(self, c2d_receive_ack_scenario): + service_client = c2d_receive_ack_scenario[0] + sample_c2d_receive = c2d_receive_ack_scenario[1] - etag = "3k28zb44-0d00-4ddd-ade3-6110eb94c476" - service_client.return_value.status_code = 204 + timeout = 120 + for ack in ["complete", "reject", "abandon"]: + result = subject.iot_c2d_message_receive( + fixture_cmd, + device_id, + mock_target["entity"], + timeout, + complete=(ack == 'complete'), + reject=(ack == 'reject'), + abandon=(ack == 'abandon') + ) + retrieve, action = service_client.calls[0], service_client.calls[1] + + # retrieve call + request = retrieve.request + url = request.url + headers = request.headers + assert ( + "{}/devices/{}/messages/deviceBound?".format( + mock_target["entity"], device_id + ) + in url + ) + assert headers["IotHub-MessageLockTimeout"] == str(timeout) + + assert result["properties"]["system"]["iothub-ack"] == sample_c2d_receive["headers"]["iothub-ack"] + assert result["properties"]["system"]["iothub-correlationid"] == sample_c2d_receive["headers"]["iothub-correlationid"] + assert result["properties"]["system"]["iothub-deliverycount"] == sample_c2d_receive["headers"]["iothub-deliverycount"] + assert result["properties"]["system"]["iothub-expiry"] == sample_c2d_receive["headers"]["iothub-expiry"] + assert result["properties"]["system"]["iothub-enqueuedtime"] == sample_c2d_receive["headers"]["iothub-enqueuedtime"] + assert result["properties"]["system"]["iothub-messageid"] == sample_c2d_receive["headers"]["iothub-messageid"] + assert ( + result["properties"]["system"]["iothub-sequencenumber"] + == sample_c2d_receive["headers"]["iothub-sequencenumber"] + ) + assert result["properties"]["system"]["iothub-userid"] == sample_c2d_receive["headers"]["iothub-userid"] + assert result["properties"]["system"]["iothub-to"] == sample_c2d_receive["headers"]["iothub-to"] + + assert result["etag"] == sample_c2d_receive["headers"]["etag"].strip('"') + + if sample_c2d_receive.get("body"): + assert result["data"] == sample_c2d_receive["body"] + + # ack call - complete / reject / abandon + request = action.request + url = request.url + headers = request.headers + method = request.method + + # all ack calls go to the same base URL + assert ( + "{}/devices/{}/messages/deviceBound/".format( + mock_target["entity"], device_id + ) + in url + ) + # check complete + if ack == "complete": + assert method == "DELETE" + # check reject + if ack == "reject": + assert method == "DELETE" + assert "reject=" in url + # check abandon + if ack == "abandon": + assert method == "POST" + assert "/abandon" in url + service_client.calls.reset() + + def test_c2d_receive_ack_errors(self): + with pytest.raises(CLIError): + subject.iot_c2d_message_receive( + fixture_cmd, + device_id, + hub_name=mock_target["entity"], + abandon=True, + complete=True, + ) + subject.iot_c2d_message_receive( + fixture_cmd, + device_id, + hub_name=mock_target["entity"], + abandon=False, + complete=True, + reject=True, + ) + subject.iot_c2d_message_receive( + fixture_cmd, + device_id, + hub_name=mock_target["entity"], + complete=True, + reject=True, + ) + + def test_c2d_complete(self, c2d_ack_complete_scenario): + service_client = c2d_ack_complete_scenario result = subject.iot_c2d_message_complete( - fixture_cmd, device_id, etag, hub_name=mock_target["entity"] + fixture_cmd, device_id, message_etag, hub_name=mock_target["entity"] ) - args = service_client.call_args - url = args[0][0].url - method = args[0][0].method + request = service_client.calls[0].request + url = request.url + method = request.method assert result is None assert method == "DELETE" assert ( "{}/devices/{}/messages/deviceBound/{}?".format( - mock_target["entity"], device_id, etag + mock_target["entity"], device_id, message_etag ) in url ) - def test_c2d_reject(self, fixture_service_client_generic): - service_client = fixture_service_client_generic + def test_c2d_reject(self, c2d_ack_reject_scenario): + service_client = c2d_ack_reject_scenario - etag = "3k28zb44-0d00-4ddd-ade3-6110eb94c476" - service_client.return_value.status_code = 204 result = subject.iot_c2d_message_reject( - fixture_cmd, device_id, etag, hub_name=mock_target["entity"] + fixture_cmd, device_id, message_etag, hub_name=mock_target["entity"] ) - args = service_client.call_args - url = args[0][0].url - method = args[0][0].method + request = service_client.calls[0].request + url = request.url + method = request.method assert result is None assert method == "DELETE" assert ( "{}/devices/{}/messages/deviceBound/{}?".format( - mock_target["entity"], device_id, etag + mock_target["entity"], device_id, message_etag ) in url ) assert "reject=" in url - def test_c2d_abandon(self, fixture_service_client_generic): - service_client = fixture_service_client_generic + def test_c2d_abandon(self, c2d_ack_abandon_scenario): + service_client = c2d_ack_abandon_scenario - etag = "3k28zb44-0d00-4ddd-ade3-6110eb94c476" - service_client.return_value.status_code = 204 result = subject.iot_c2d_message_abandon( - fixture_cmd, device_id, etag, hub_name=mock_target["entity"] + fixture_cmd, device_id, message_etag, hub_name=mock_target["entity"] ) - args = service_client.call_args - url = args[0][0].url - method = args[0][0].method + request = service_client.calls[0].request + url = request.url + method = request.method assert result is None assert method == "POST" assert ( "{}/devices/{}/messages/deviceBound/{}/abandon?".format( - mock_target["entity"], device_id, etag + mock_target["entity"], device_id, message_etag ) in url ) @@ -1572,6 +1770,21 @@ def test_c2d_receive_and_ack_errors(self, serviceclient_generic_error): fixture_cmd, device_id, hub_name=mock_target["entity"], etag="" ) + def test_c2d_message_purge(self, c2d_purge_scenario): + result = subject.iot_c2d_message_purge(fixture_cmd, device_id) + request = c2d_purge_scenario.calls[0].request + url = request.url + method = request.method + + assert method == "DELETE" + assert "https://{}/devices/{}/commands".format( + mock_target["entity"], device_id + ) in url + assert result + assert result.total_messages_purged == 3 + assert result.device_id == device_id + assert not result.module_id + class TestSasTokenAuth: def test_generate_sas_token(self): diff --git a/azext_iot/tests/test_iot_messaging_int.py b/azext_iot/tests/test_iot_messaging_int.py index ae5e9743e..94c961015 100644 --- a/azext_iot/tests/test_iot_messaging_int.py +++ b/azext_iot/tests/test_iot_messaging_int.py @@ -742,3 +742,52 @@ def test_hub_monitor_feedback(self): "iot hub monitor-feedback --login {} -w {} -y".format(self.connection_string, msg_id), ["description: Message rejected"], ) + + # purge messages + num_messages = 3 + for i in range(num_messages): + self.cmd( + "iot device c2d-message send -d {} --login {}".format( + device_ids[0], self.connection_string + ), + checks=self.is_empty(), + ) + purge_result = self.cmd( + "iot device c2d-message purge -d {} --login {}".format( + device_ids[0], self.connection_string + ) + ).get_output_in_json() + assert purge_result["deviceId"] == device_ids[0] + assert purge_result["totalMessagesPurged"] == num_messages + assert not purge_result["moduleId"] + + # Errors with multiple ack arguments + self.cmd( + "iot device c2d-message receive -d {} --login {} --complete --abandon", + expect_failure=True, + ) + self.cmd( + "iot device c2d-message receive -d {} --login {} --reject --abandon", + expect_failure=True, + ) + self.cmd( + "iot device c2d-message receive -d {} --login {} --reject --complete --abandon", + expect_failure=True, + ) + + # Receive with auto-ack + for ack_test in ["complete", "abandon", "reject"]: + self.cmd( + "iot device c2d-message send -d {} --login {}".format( + device_ids[0], self.connection_string + ), + checks=self.is_empty(), + ) + result = self.cmd( + "iot device c2d-message receive -d {} --login {} --{}".format( + device_ids[0], self.connection_string, ack_test + ) + ).get_output_in_json() + assert result["ack"] == ack_test + assert json.dumps(result["data"]) + assert json.dumps(result["properties"]["system"]) From 67bd50cfa2e33691f836463dbf257c7b724cd3b6 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Thu, 20 Aug 2020 17:42:17 -0700 Subject: [PATCH 100/179] Edge device creation - enable x509 authentication types (#239) * Enable edge device creation with x509 auth * History update --- HISTORY.rst | 3 ++ azext_iot/operations/hub.py | 2 - azext_iot/tests/test_iot_ext_int.py | 52 ++++++++++++++++++----- azext_iot/tests/test_iot_ext_unit.py | 6 ++- azext_iot/tests/test_iot_messaging_int.py | 12 ++++-- 5 files changed, 57 insertions(+), 18 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ae900f172..5af8b4e6e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -27,6 +27,9 @@ Cloud-to-Device message enhancements * Options are `--complete`, `--abandon`, and `--reject`, and only one can be used per command. * `az iot device c2d-message receive` with no ack arguments remains unchanged and will not ack the message. +Edge device creation enhancements +* Enabled x509 self-signed certificate authentication types (`x509_thumbprint` and `x509_ca`) for edge device creation with `az iot hub device-identity create --ee` + 0.9.8 +++++++++++++++ General changes diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 671b1d1d2..256bf2fa3 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -140,8 +140,6 @@ def iot_device_create( deviceScope = None if edge_enabled: - if auth_method != DeviceAuthType.shared_private_key.name: - raise CLIError("currently edge devices are limited to symmetric key auth") if add_children: for non_edge_device_id in add_children.split(","): nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 0297c0b52..4503f4d46 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -149,10 +149,14 @@ def __init__(self, test_case): 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) - total_devices = device_count + edge_device_count + 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( @@ -199,25 +203,51 @@ def test_hub_devices(self): ) # All edge devices + child device - query_checks = [self.check("length([*])", edge_device_count + 1)] + 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]))) - # Not currently supported + # Edge x509_thumbprint self.cmd( - "iot hub device-identity create -d {} -n {} -g {} --auth-method x509_thumbprint --ee".format( - "willnotwork", LIVE_HUB, LIVE_RG + "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 ), - expect_failure=True, + 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, + ), + ] ) - # Not currently supported + # Edge x509_ca self.cmd( "iot hub device-identity create -d {} -n {} -g {} --auth-method x509_ca --ee".format( - "willnotwork", LIVE_HUB, LIVE_RG + edge_x509_device_ids[1], LIVE_HUB, LIVE_RG ), - expect_failure=True, + 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( @@ -383,13 +413,13 @@ def test_hub_devices(self): # List only edge devices self.cmd( "iot hub device-identity list -n {} -g {} --ee".format(LIVE_HUB, LIVE_RG), - checks=[self.check("length([*])", edge_device_count)], + 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([*])", edge_device_count)], + checks=[self.check("length([*])", total_edge_device_count)], ) self.cmd( diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index 5b6abcd64..faf9ea762 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -100,6 +100,9 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): [ (generate_device_create_req()), (generate_device_create_req(ee=True)), + (generate_device_create_req(ee=True, auth="x509_ca")), + (generate_device_create_req(ee=True, auth="x509_thumbprint")), + (generate_device_create_req(ee=True, auth="x509_thumbprint", stp=None)), (generate_device_create_req(auth="x509_ca")), (generate_device_create_req(auth="x509_thumbprint")), (generate_device_create_req(auth="x509_thumbprint", stp=None)), @@ -317,8 +320,7 @@ def test_device_create_addchildren_invalid_args( @pytest.mark.parametrize( "req, exp", [ - (generate_device_create_req(ee=True, auth="x509_thumbprint"), CLIError), - (generate_device_create_req(ee=True, auth="x509_ca"), CLIError), + (generate_device_create_req(ee=True, auth="x509_thumbprint", ptp=None), ValueError), (generate_device_create_req(auth="doesnotexist"), ValueError), ( generate_device_create_req(auth="x509_thumbprint", ptp=None, stp=""), diff --git a/azext_iot/tests/test_iot_messaging_int.py b/azext_iot/tests/test_iot_messaging_int.py index 94c961015..286948d95 100644 --- a/azext_iot/tests/test_iot_messaging_int.py +++ b/azext_iot/tests/test_iot_messaging_int.py @@ -763,15 +763,21 @@ def test_hub_monitor_feedback(self): # Errors with multiple ack arguments self.cmd( - "iot device c2d-message receive -d {} --login {} --complete --abandon", + "iot device c2d-message receive -d {} --login {} --complete --abandon".format( + device_ids[0], self.connection_string + ), expect_failure=True, ) self.cmd( - "iot device c2d-message receive -d {} --login {} --reject --abandon", + "iot device c2d-message receive -d {} --login {} --reject --abandon".format( + device_ids[0], self.connection_string + ), expect_failure=True, ) self.cmd( - "iot device c2d-message receive -d {} --login {} --reject --complete --abandon", + "iot device c2d-message receive -d {} --login {} --reject --complete --abandon".format( + device_ids[0], self.connection_string + ), expect_failure=True, ) From 28567be4662f8b9f817e7257b010f436785aadac Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 21 Aug 2020 14:01:52 -0700 Subject: [PATCH 101/179] Support providing explicit hostname for ADT dataplane ops. (#240) --- .pylintrc | 6 +- HISTORY.rst | 20 ++++- azext_iot/common/utility.py | 17 +++- azext_iot/constants.py | 2 +- azext_iot/digitaltwins/_help.py | 6 +- azext_iot/digitaltwins/commands_models.py | 20 ++--- azext_iot/digitaltwins/commands_routes.py | 16 ++-- azext_iot/digitaltwins/commands_twins.py | 61 +++++++------ azext_iot/digitaltwins/params.py | 7 ++ azext_iot/digitaltwins/providers/base.py | 24 +++-- azext_iot/tests/conftest.py | 26 +++++- .../digitaltwins/test_dt_provider_unit.py | 87 +++++++++++++++++++ 12 files changed, 233 insertions(+), 59 deletions(-) create mode 100644 azext_iot/tests/digitaltwins/test_dt_provider_unit.py diff --git a/.pylintrc b/.pylintrc index 3bfaf5a3d..62bb79bf5 100644 --- a/.pylintrc +++ b/.pylintrc @@ -11,7 +11,7 @@ ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns= +ignore-patterns=sdk # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). @@ -279,7 +279,9 @@ disable=import-outside-toplevel, deprecated-sys-function, exception-escape, comprehension-escape, - not-callable + not-callable, + raise-missing-from, + super-with-arguments # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/HISTORY.rst b/HISTORY.rst index 5af8b4e6e..56717b070 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,29 +7,45 @@ Release History +++++++++++++++ IoT Central updates + * Current release involves re-grouping of IoT central commands. + * 'az iot central app device-twin' deprecated use 'az iot central device twin' instead. Deprecated command group is planned to be removed by December 2020 * 'az iot central app monitor-events' deprecated use 'az iot central diagnostics monitor-events' instead. Deprecated command is planned to be removed by December 2020 - * commands listed below are in preview + * Note all commands listed below are in preview + * Introduces 'az iot central diagnostics' preview command group to perform application and device level diagnostics + * 'az iot central app device registration-summary' moved to 'az iot central diagnostics registration-summary' * 'az iot central app monitor-properties' moved to 'az iot central diagnostics monitor-properties' * 'az iot central app validate-messages' moved to 'az iot central diagnostics validate-messages' * 'az iot central app validate-properties' moved to 'az iot central diagnostics validate-properties' * 'az iot central diagnostics monitor-events' added to support deprecation of 'az iot central app monitor-events' + * 'az iot central app device run-command' moved to 'az iot central device command run' * 'az iot central app device show-command-history' moved to 'az iot central device command history' * 'az iot central device twin' added to support deprecation of 'az iot central app device-twin' command group - + Cloud-to-Device message enhancements + * Introduced new `az iot device c2d-message purge` command to purge the message queue for a device. * Added message ack arguments to `az iot c2d-message receive` to ack the message after it is received: + * Options are `--complete`, `--abandon`, and `--reject`, and only one can be used per command. * `az iot device c2d-message receive` with no ack arguments remains unchanged and will not ack the message. Edge device creation enhancements + * Enabled x509 self-signed certificate authentication types (`x509_thumbprint` and `x509_ca`) for edge device creation with `az iot hub device-identity create --ee` +Digital Twins updates + +* The 'az dt route | model | twin' command groups support passing in a DT instance hostname directly. + + * If an instance name is provided, the user subscription is first queried for the target instance to retrieve the hostname. + * If a hostname is provided, the subscription query is skipped and the provided value is used for subsequent interaction. + + 0.9.8 +++++++++++++++ General changes diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index 8c34f25cb..d03694312 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------------------------- """ -utility: Define helper functions for 'common' scripts. +utility: Defines common utility functions and components. """ @@ -15,6 +15,7 @@ import json import os import sys +import re from threading import Event, Thread from datetime import datetime @@ -480,3 +481,17 @@ def scantree(path): def find_between(s, start, end): return (s.split(start))[1].split(end)[0] + + +def valid_hostname(host_name): + """ + Approximate validation + Reference: https://en.wikipedia.org/wiki/Hostname + """ + + if len(host_name) > 253: + return False + + valid_label = re.compile(r"(?!-)[A-Z\d-]{1,63}(? - az dt twin create -n {instance_name} --dtmi urn:azureiot:DeviceManagement:DeviceInformation:1 + az dt twin create -n {instance_name} --dtmi dtmi:example:Room;1 --twin-id {twin_id} - name: Create a digital twin from an existing (prior-created) model. Instantiate with property values. text: > - az dt twin create -n {instance_name} --dtmi urn:azureiot:DeviceManagement:DeviceInformation:1 + az dt twin create -n {instance_name} --dtmi dtmi:com:example:DeviceInformation;1 --twin-id {twin_id} --properties '{"manufacturer": "Microsoft"}' - name: Create a digital twin with component from existing (prior-created) models. Instantiate with property values. text: > - az dt twin create -n {instance_name} --dtmi dtmi:example:Room;1 --twin-id {twin_id} --properties '{ + az dt twin create -n {instance_name} --dtmi dtmi:com:example:TemperatureController;1 --twin-id {twin_id} --properties '{ "Temperature": 10.2, "Thermostat": { "$metadata": {}, diff --git a/azext_iot/digitaltwins/commands_models.py b/azext_iot/digitaltwins/commands_models.py index 71e938e0a..bdf6782e0 100644 --- a/azext_iot/digitaltwins/commands_models.py +++ b/azext_iot/digitaltwins/commands_models.py @@ -10,35 +10,35 @@ logger = get_logger(__name__) -def add_models(cmd, name, models=None, from_directory=None, resource_group_name=None): - model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) +def add_models(cmd, name_or_hostname, models=None, from_directory=None, resource_group_name=None): + model_provider = ModelProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) logger.debug("Received models input: %s", models) return model_provider.add(models=models, from_directory=from_directory) -def show_model(cmd, name, model_id, definition=False, resource_group_name=None): - model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) +def show_model(cmd, name_or_hostname, model_id, definition=False, resource_group_name=None): + model_provider = ModelProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return model_provider.get(id=model_id, get_definition=definition) def list_models( - cmd, name, definition=False, dependencies_for=None, resource_group_name=None + cmd, name_or_hostname, definition=False, dependencies_for=None, resource_group_name=None ): - model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) + model_provider = ModelProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return model_provider.list( get_definition=definition, dependencies_for=dependencies_for ) -def update_model(cmd, name, model_id, decommission=None, resource_group_name=None): +def update_model(cmd, name_or_hostname, model_id, decommission=None, resource_group_name=None): if decommission is None: logger.info("No update arguments provided. Nothing to update.") return - model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) + model_provider = ModelProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return model_provider.update(id=model_id, decommission=decommission,) -def delete_model(cmd, name, model_id, resource_group_name=None): - model_provider = ModelProvider(cmd=cmd, name=name, rg=resource_group_name) +def delete_model(cmd, name_or_hostname, model_id, resource_group_name=None): + model_provider = ModelProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return model_provider.delete(id=model_id) diff --git a/azext_iot/digitaltwins/commands_routes.py b/azext_iot/digitaltwins/commands_routes.py index 40c390e34..923ffd57d 100644 --- a/azext_iot/digitaltwins/commands_routes.py +++ b/azext_iot/digitaltwins/commands_routes.py @@ -11,24 +11,24 @@ def create_route( - cmd, name, route_name, endpoint_name, filter="true", resource_group_name=None + cmd, name_or_hostname, route_name, endpoint_name, filter="true", resource_group_name=None ): - route_provider = RouteProvider(cmd=cmd, name=name, rg=resource_group_name) + route_provider = RouteProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return route_provider.create( route_name=route_name, endpoint_name=endpoint_name, filter=filter ) -def show_route(cmd, name, route_name, resource_group_name=None): - route_provider = RouteProvider(cmd=cmd, name=name, rg=resource_group_name) +def show_route(cmd, name_or_hostname, route_name, resource_group_name=None): + route_provider = RouteProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return route_provider.get(route_name=route_name) -def list_routes(cmd, name, resource_group_name=None): - route_provider = RouteProvider(cmd=cmd, name=name, rg=resource_group_name) +def list_routes(cmd, name_or_hostname, resource_group_name=None): + route_provider = RouteProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return route_provider.list() -def delete_route(cmd, name, route_name, resource_group_name=None): - route_provider = RouteProvider(cmd=cmd, name=name, rg=resource_group_name) +def delete_route(cmd, name_or_hostname, route_name, resource_group_name=None): + route_provider = RouteProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return route_provider.delete(route_name=route_name) diff --git a/azext_iot/digitaltwins/commands_twins.py b/azext_iot/digitaltwins/commands_twins.py index f924ad00e..c6f0132fd 100644 --- a/azext_iot/digitaltwins/commands_twins.py +++ b/azext_iot/digitaltwins/commands_twins.py @@ -10,38 +10,40 @@ logger = get_logger(__name__) -def query_twins(cmd, name, query_command, show_cost=False, resource_group_name=None): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) +def query_twins( + cmd, name_or_hostname, query_command, show_cost=False, resource_group_name=None +): + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.invoke_query(query=query_command, show_cost=show_cost) def create_twin( - cmd, name, twin_id, model_id, properties=None, resource_group_name=None + cmd, name_or_hostname, twin_id, model_id, properties=None, resource_group_name=None ): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.create( twin_id=twin_id, model_id=model_id, properties=properties ) -def show_twin(cmd, name, twin_id, resource_group_name=None): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) +def show_twin(cmd, name_or_hostname, twin_id, resource_group_name=None): + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.get(twin_id) -def update_twin(cmd, name, twin_id, json_patch, resource_group_name=None): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) +def update_twin(cmd, name_or_hostname, twin_id, json_patch, resource_group_name=None): + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.update(twin_id=twin_id, json_patch=json_patch) -def delete_twin(cmd, name, twin_id, resource_group_name=None): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) +def delete_twin(cmd, name_or_hostname, twin_id, resource_group_name=None): + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.delete(twin_id) def create_relationship( cmd, - name, + name_or_hostname, twin_id, target_twin_id, relationship_id, @@ -49,7 +51,7 @@ def create_relationship( properties=None, resource_group_name=None, ): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.add_relationship( twin_id=twin_id, target_twin_id=target_twin_id, @@ -60,18 +62,23 @@ def create_relationship( def show_relationship( - cmd, name, twin_id, relationship_id, resource_group_name=None, + cmd, name_or_hostname, twin_id, relationship_id, resource_group_name=None, ): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.get_relationship( twin_id=twin_id, relationship_id=relationship_id ) def update_relationship( - cmd, name, twin_id, relationship_id, json_patch, resource_group_name=None + cmd, + name_or_hostname, + twin_id, + relationship_id, + json_patch, + resource_group_name=None, ): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.update_relationship( twin_id=twin_id, relationship_id=relationship_id, json_patch=json_patch, ) @@ -79,13 +86,13 @@ def update_relationship( def list_relationships( cmd, - name, + name_or_hostname, twin_id, incoming_relationships=False, relationship=None, resource_group_name=None, ): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.list_relationships( twin_id=twin_id, incoming_relationships=incoming_relationships, @@ -94,9 +101,9 @@ def list_relationships( def delete_relationship( - cmd, name, twin_id, relationship_id, resource_group_name=None, + cmd, name_or_hostname, twin_id, relationship_id, resource_group_name=None, ): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.delete_relationship( twin_id=twin_id, relationship_id=relationship_id ) @@ -104,28 +111,30 @@ def delete_relationship( def send_telemetry( cmd, - name, + name_or_hostname, twin_id, dt_id=None, component_path=None, telemetry=None, resource_group_name=None, ): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.send_telemetry( twin_id=twin_id, dt_id=dt_id, component_path=component_path, telemetry=telemetry ) -def show_component(cmd, name, twin_id, component_path, resource_group_name=None): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) +def show_component( + cmd, name_or_hostname, twin_id, component_path, resource_group_name=None +): + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.get_component(twin_id=twin_id, component_path=component_path) def update_component( - cmd, name, twin_id, component_path, json_patch, resource_group_name=None + cmd, name_or_hostname, twin_id, component_path, json_patch, resource_group_name=None ): - twin_provider = TwinProvider(cmd=cmd, name=name, rg=resource_group_name) + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) twin_provider.update_component( twin_id=twin_id, component_path=component_path, json_patch=json_patch ) diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index a15ae9336..45600ea17 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -39,6 +39,13 @@ def load_digitaltwins_arguments(self, _): options_list=["-n", "--dtn", "--dt-name"], help="Digital Twins instance name.", ) + context.argument( + "name_or_hostname", + options_list=["-n", "--dtn", "--dt-name"], + help="Digital Twins instance name or hostname. If an instance name is provided, the user subscription is " + "first queried for the target instance to retrieve the hostname. If a hostname is provided, the " + "subscription query is skipped and the provided value is used for subsequent interaction.", + ) context.argument( "location", options_list=["--location", "-l"], diff --git a/azext_iot/digitaltwins/providers/base.py b/azext_iot/digitaltwins/providers/base.py index 1909c2e40..e7d21a40e 100644 --- a/azext_iot/digitaltwins/providers/base.py +++ b/azext_iot/digitaltwins/providers/base.py @@ -9,6 +9,7 @@ from azext_iot.sdk.digitaltwins import AzureDigitalTwinsAPI from azext_iot.sdk.digitaltwins.models import ErrorResponseException from azext_iot.constants import DIGITALTWINS_RESOURCE_ID +from azext_iot.common.utility import valid_hostname from knack.cli import CLIError __all__ = ["DigitalTwinsProvider", "ErrorResponseException"] @@ -26,11 +27,24 @@ def __init__(self, cmd, name, rg=None): self.rp = ResourceProvider(self.cmd) def _get_endpoint(self): - instance = self.rp.find_instance(name=self.name, resource_group_name=self.rg) - host_name = instance.host_name - if not host_name: - raise CLIError("Retrieved hostName was null which is invalid.") - return "https://{}".format(instance.host_name) + host_name = None + https_prefix = "https://" + http_prefix = "http://" + + if self.name.lower().startswith(https_prefix): + self.name = self.name[len(https_prefix):] + elif self.name.lower().startswith(http_prefix): + self.name = self.name[len(http_prefix):] + + if not all([valid_hostname(self.name), "." in self.name]): + instance = self.rp.find_instance(name=self.name, resource_group_name=self.rg) + host_name = instance.host_name + if not host_name: + raise CLIError("Instance has invalid hostName. Aborting operation...") + else: + host_name = self.name + + return "https://{}".format(host_name) def get_sdk(self): creds = DigitalTwinAuthentication(cmd=self.cmd, resource_id=self.resource_id) diff --git a/azext_iot/tests/conftest.py b/azext_iot/tests/conftest.py index e0bf222ac..6860b4c6c 100644 --- a/azext_iot/tests/conftest.py +++ b/azext_iot/tests/conftest.py @@ -17,7 +17,9 @@ path_iot_hub_service_factory = "azext_iot._factory.iot_hub_service_factory" path_service_client = "msrest.service_client.ServiceClient.send" path_ghcs = "azext_iot.iothub.providers.discovery.IotHubDiscovery.get_target" -path_discovery_init = "azext_iot.iothub.providers.discovery.IotHubDiscovery._initialize_client" +path_discovery_init = ( + "azext_iot.iothub.providers.discovery.IotHubDiscovery._initialize_client" +) path_sas = "azext_iot._factory.SasTokenAuthentication" path_mqtt_client = "azext_iot.operations._mqtt.mqtt.Client" path_iot_hub_monitor_events_entrypoint = ( @@ -177,6 +179,28 @@ def get_context_path(base_path, *paths): return base_path +''' TODO: Possibly expand for future use +fake_oauth_response = responses.Response( + method=responses.POST, + url=re.compile("https://login.microsoftonline.com/(.+)/oauth2/token"), + body=json.dumps({ + "token_type": "Bearer", + "scope": "user_impersonation", + "expires_in": "90000", + "ext_expires_in": "90000", + "expires_on": "979778250", + "not_before": "979739250", + "resource": "localhost", + "access_token": "totally_fake_access_token", + "refresh_token": "totally_fake_refresh_token", + "foci": "1" + }), + status=200, + content_type="application/json", +) +''' + + @pytest.fixture def mocked_response(): with responses.RequestsMock() as rsps: diff --git a/azext_iot/tests/digitaltwins/test_dt_provider_unit.py b/azext_iot/tests/digitaltwins/test_dt_provider_unit.py new file mode 100644 index 000000000..6eb851def --- /dev/null +++ b/azext_iot/tests/digitaltwins/test_dt_provider_unit.py @@ -0,0 +1,87 @@ +# 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 azext_iot.digitaltwins.providers.base import DigitalTwinsProvider +from ..generators import generate_generic_id + + +resource_group = generate_generic_id() +instance_name = generate_generic_id() +qualified_hostname = "{}.subdomain.domain".format(instance_name) + + +@pytest.fixture +def get_mgmt_client(mocker, fixture_cmd): + from azext_iot.sdk.digitaltwins_arm import AzureDigitalTwinsManagementClient + from azext_iot.digitaltwins.providers.auth import DigitalTwinAuthentication + + 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"), + ) + + patch = mocker.patch( + "azext_iot.digitaltwins.providers.digitaltwins_service_factory" + ) + patch.return_value = AzureDigitalTwinsManagementClient( + credentials=DigitalTwinAuthentication( + fixture_cmd, "00000000-0000-0000-0000-000000000000" + ), + subscription_id="00000000-0000-0000-0000-000000000000", + ) + + return patch + + +class TestDigitalTwinsProvider: + @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/(.*)/resourceGroups/{}/" + "providers/Microsoft.DigitalTwins/digitalTwinsInstances/{}".format( + resource_group, instance_name + ) + ), + status=200, + match_querystring=False, + body=json.dumps({"hostName": qualified_hostname}), + ) + + yield mocked_response + + @pytest.mark.parametrize( + "name, expected", + [ + (instance_name, "https://{}".format(qualified_hostname)), + (qualified_hostname, "https://{}".format(qualified_hostname)), + ( + "https://{}.domain".format(instance_name), + "https://{}.domain".format(instance_name), + ), + ( + "http://{}.domain".format(instance_name), + "https://{}.domain".format(instance_name), + ), + ], + ) + def test_get_endpoint(self, fixture_cmd, name, expected, service_client): + subject = DigitalTwinsProvider(cmd=fixture_cmd, name=name, rg=resource_group) + endpoint = subject._get_endpoint() + + assert endpoint == expected From 9885dd528fe363f5c118f3b735ad22fd4e1a86f4 Mon Sep 17 00:00:00 2001 From: valluriraj Date: Tue, 25 Aug 2020 11:34:28 -0700 Subject: [PATCH 102/179] Add dps keygen command (#241) * add support for compute-device-key dps / central * integration test updates * help example fix --- HISTORY.rst | 6 +- azext_iot/_help.py | 19 + azext_iot/_params.py | 14 +- azext_iot/central/_help.py | 22 + azext_iot/central/command_map.py | 7 + azext_iot/central/commands_device.py | 6 + azext_iot/central/params.py | 7 + azext_iot/commands.py | 13 +- azext_iot/common/utility.py | 20 + azext_iot/operations/dps.py | 776 +++++++++++++++--------- azext_iot/tests/test_iot_central_int.py | 34 +- 11 files changed, 617 insertions(+), 307 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 56717b070..b49b75af9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,10 @@ Release History 0.9.9 +++++++++++++++ +IoT dps updates + * Introduces 'az iot dps compute-device-key' preview command group to generate derived device SAS key + + IoT Central updates * Current release involves re-grouping of IoT central commands. @@ -15,7 +19,7 @@ IoT Central updates * Note all commands listed below are in preview * Introduces 'az iot central diagnostics' preview command group to perform application and device level diagnostics - + * Introduces 'az iot central device compute-device-key' preview command group to generate derived device SAS key * 'az iot central app device registration-summary' moved to 'az iot central diagnostics registration-summary' * 'az iot central app monitor-properties' moved to 'az iot central diagnostics monitor-properties' * 'az iot central app validate-messages' moved to 'az iot central diagnostics validate-messages' diff --git a/azext_iot/_help.py b/azext_iot/_help.py index bf751f685..a0614c334 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -1238,3 +1238,22 @@ type: command short-summary: Delete a device registration in an Azure IoT Hub Device Provisioning Service. """ + +helps[ + "iot dps compute-device-key" +] = """ + type: group + short-summary: Generate a derived device SAS key +""" + +helps[ + "iot dps compute-device-key create" +] = """ + type: command + short-summary: Generate a derived device SAS key. + Long-summary: Generate a derived device key from a DPS enrollment group SAS token. + examples: + - name: basic usage + text: > + az iot dps compute-device-key create --pk {primaryKey} --registration-id {registrationId} +""" diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 60cbe1c8e..a714d63a2 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -267,13 +267,13 @@ def load_arguments(self, _): context.argument( "show_all", options_list=["--show-all", "--all"], - help="Allow to show all shared access policies." + help="Allow to show all shared access policies.", ) context.argument( "default_eventhub", arg_type=get_three_state_flag(), options_list=["--default-eventhub", "--eh"], - help="Flag indicating the connection string returned is for the default EventHub endpoint. Default: false" + help="Flag indicating the connection string returned is for the default EventHub endpoint. Default: false", ) with self.argument_context("iot hub job") as context: @@ -809,9 +809,17 @@ def load_arguments(self, _): "api_version", options_list=["--api-version", "--av"], help="The API version of the provisioning service types sent in the custom allocation" - " request. Minimum supported version: 2018-09-01-preview." + " request. Minimum supported version: 2018-09-01-preview.", ) + with self.argument_context("iot dps compute-device-key") as context: + context.argument( + "primary_key", + options_list=["--primary-key", "--pk"], + help="The primary symmetric shared access key stored in base64 format. ", + ) + context.argument("registration_id", help="ID of device registration") + with self.argument_context("iot dps enrollment") as context: context.argument("enrollment_id", help="ID of device enrollment record") context.argument("device_id", help="IoT Hub Device ID") diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 9ee722317..bbc056c01 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -38,6 +38,7 @@ def load_central_help(): _load_central_device_templates_help() _load_central_monitors_help() _load_central_command_help() + _load_central_compute_device_key() # TODO: Delete this by end of Dec 2020 _load_central_deprecated_commands() @@ -130,6 +131,27 @@ def _load_central_devices_help(): """ +def _load_central_compute_device_key(): + helps[ + "iot central device compute-device-key" + ] = """ + type: group + short-summary: Generate a derived device SAS key + """ + + helps[ + "iot central device compute-device-key create" + ] = """ + type: command + short-summary: Generate a derived device SAS key. + Long-summary: Generate a derived device key from primary group SAS token. + examples: + - name: basic usage + text: > + az iot central device compute-device-key create --pk {primaryKey} --device-id {deviceid} + """ + + def _load_central_command_help(): helps[ "iot central device command" diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index 90ff0a083..1048229a3 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -91,6 +91,13 @@ def load_central_commands(self, _): cmd_group.command("run", "run_command") cmd_group.command("history", "get_command_history") + with self.command_group( + "iot central device compute-device-key", + command_type=central_device_ops, + is_preview=True, + ) as cmd_group: + cmd_group.command("create", "compute_device_key") + with self.command_group( "iot central device-template", command_type=central_device_templates_ops, diff --git a/azext_iot/central/commands_device.py b/azext_iot/central/commands_device.py index e1a45012b..69e6026c5 100644 --- a/azext_iot/central/commands_device.py +++ b/azext_iot/central/commands_device.py @@ -120,3 +120,9 @@ def get_credentials( return provider.get_device_credentials( device_id=device_id, central_dns_suffix=central_dns_suffix, ) + + +def compute_device_key(cmd, primary_key, device_id): + return utility.compute_device_key( + primary_key=primary_key, registration_id=device_id + ) diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 0d465db69..02cceb5a8 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -78,6 +78,13 @@ def load_central_arguments(self, _): ) context.argument("role", arg_type=role_type) + with self.argument_context("iot central device compute-device-key") as context: + context.argument( + "primary_key", + options_list=["--primary-key", "--pk"], + help="The primary symmetric shared access key stored in base64 format. ", + ) + with self.argument_context("iot central device") as context: context.argument( "instance_of", diff --git a/azext_iot/commands.py b/azext_iot/commands.py index c1071de43..0b72cb3df 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -40,7 +40,9 @@ def load_command_table(self, _): cmd_group.command( "show-connection-string", "iot_get_device_connection_string", - deprecate_info=self.deprecate(redirect='az iot hub device-identity connection-string show') + deprecate_info=self.deprecate( + redirect="az iot hub device-identity connection-string show" + ), ) cmd_group.command("import", "iot_device_import") cmd_group.command("export", "iot_device_export") @@ -71,7 +73,9 @@ def load_command_table(self, _): cmd_group.command( "show-connection-string", "iot_get_module_connection_string", - deprecate_info=self.deprecate(redirect='az iot hub module-identity connection-string show') + deprecate_info=self.deprecate( + redirect="az iot hub module-identity connection-string show" + ), ) with self.command_group( @@ -184,3 +188,8 @@ def load_command_table(self, _): cmd_group.command("list", "iot_dps_registration_list") cmd_group.command("show", "iot_dps_registration_get") cmd_group.command("delete", "iot_dps_registration_delete") + + with self.command_group( + "iot dps compute-device-key", command_type=iotdps_ops, is_preview=True + ) as cmd_group: + cmd_group.command("create", "iot_dps_compute_device_key") diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index d03694312..a939d5855 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -16,6 +16,8 @@ import os import sys import re +import hmac +import hashlib from threading import Event, Thread from datetime import datetime @@ -495,3 +497,21 @@ def valid_hostname(host_name): valid_label = re.compile(r"(?!-)[A-Z\d-]{1,63}(? 1: - raise CLIError('Only one hub is required in static allocation policy.') + raise CLIError("Only one hub is required in static allocation policy.") if allocation_policy == AllocationType.custom.value: if webhook_url is None or api_version is None: - raise CLIError('Please provide both the Azure function webhook url and provisioning' - ' service api-version when the allocation-policy is defined as Custom.') + raise CLIError( + "Please provide both the Azure function webhook url and provisioning" + " service api-version when the allocation-policy is defined as Custom." + ) else: if iot_hub_list: - raise CLIError('Please provide allocation policy.') + raise CLIError("Please provide allocation policy.") diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index fc8160d55..930594712 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -8,6 +8,7 @@ import json import os import time +import pytest from .conftest import get_context_path @@ -19,7 +20,8 @@ from . import CaptureOutputLiveScenarioTest, helpers APP_ID = os.environ.get("azext_iot_central_app_id") - +APP_PRIMARY_KEY = os.environ.get("azext_iot_central_primarykey") +APP_SCOPE_ID = os.environ.get("azext_iot_central_scope_id") device_template_path = get_context_path( __file__, "central/json/device_template_int_test.json" ) @@ -185,6 +187,36 @@ def test_central_validate_messages_success(self): assert "Successfully parsed 1 message(s)" in output assert "No errors detected" in output + @pytest.mark.skipif( + not APP_SCOPE_ID, reason="empty azext_iot_central_scope_id env var" + ) + @pytest.mark.skipif( + not APP_PRIMARY_KEY, reason="empty azext_iot_central_primarykey env var" + ) + def test_device_connect(self): + device_id = "testDevice" + + device_primary_key = self.cmd( + "iot central device compute-device-key create --pk {} -d {}".format( + APP_PRIMARY_KEY, device_id + ), + ).get_output_in_json() + + credentials = { + "idScope": APP_SCOPE_ID, + "symmetricKey": {"primaryKey": device_primary_key}, + } + device_client = helpers.dps_connect_device(device_id, credentials) + + self.cmd( + "iot central device show --app-id {} -d {}".format(APP_ID, device_id), + checks=[self.check("id", device_id)], + ) + + self._delete_device(device_id) + + assert device_client.connected + def test_central_validate_messages_issues_detected(self): expected_messages = [] (template_id, _) = self._create_device_template() From 58effb55d2f9b725cfb9020437ab7648ca348e09 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 27 Aug 2020 13:22:01 -0700 Subject: [PATCH 103/179] Clean-up, cmd lint resolve, refactor and short-term fixing DPS IT. (#242) * Clean-up, lint resolve (E5001), refactor and short-term fixing DPS IT * Slightly modify language. * Fix issue #243 * Fix pytest.fail input. * Add more HISTORY content. --- HISTORY.rst | 23 +++++--- azext_iot/_help.py | 13 +---- azext_iot/_params.py | 55 ++---------------- azext_iot/central/_help.py | 13 +---- azext_iot/central/command_map.py | 20 +++---- azext_iot/commands.py | 36 ++++++------ azext_iot/common/shared.py | 10 ---- azext_iot/digitaltwins/command_map.py | 14 ++--- azext_iot/iothub/command_map.py | 4 +- azext_iot/iothub/providers/discovery.py | 9 ++- azext_iot/operations/dps.py | 4 +- azext_iot/pnp/command_map.py | 2 +- azext_iot/product/test/command_map.py | 6 +- .../iothub/test_iothub_discovery_unit.py | 58 +++++++++++++++++++ azext_iot/tests/test_iot_central_int.py | 2 +- azext_iot/tests/test_iot_dps_int.py | 21 ++++--- azext_iot/tests/test_iot_utility_unit.py | 2 +- 17 files changed, 146 insertions(+), 146 deletions(-) create mode 100644 azext_iot/tests/iothub/test_iothub_discovery_unit.py diff --git a/HISTORY.rst b/HISTORY.rst index b49b75af9..379ae5532 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,9 +6,9 @@ Release History 0.9.9 +++++++++++++++ -IoT dps updates - * Introduces 'az iot dps compute-device-key' preview command group to generate derived device SAS key +IoT DPS updates +* Introduces 'az iot dps compute-device-key' preview command to generate derived device SAS key IoT Central updates @@ -19,17 +19,20 @@ IoT Central updates * Note all commands listed below are in preview * Introduces 'az iot central diagnostics' preview command group to perform application and device level diagnostics - * Introduces 'az iot central device compute-device-key' preview command group to generate derived device SAS key - * 'az iot central app device registration-summary' moved to 'az iot central diagnostics registration-summary' - * 'az iot central app monitor-properties' moved to 'az iot central diagnostics monitor-properties' - * 'az iot central app validate-messages' moved to 'az iot central diagnostics validate-messages' - * 'az iot central app validate-properties' moved to 'az iot central diagnostics validate-properties' - * 'az iot central diagnostics monitor-events' added to support deprecation of 'az iot central app monitor-events' + * Introduces 'az iot central device compute-device-key' preview command to generate derived device SAS key + + * 'az iot central app device registration-summary' moved to 'az iot central diagnostics registration-summary' + * 'az iot central app monitor-properties' moved to 'az iot central diagnostics monitor-properties' + * 'az iot central app validate-messages' moved to 'az iot central diagnostics validate-messages' + * 'az iot central app validate-properties' moved to 'az iot central diagnostics validate-properties' + * 'az iot central diagnostics monitor-events' added to support deprecation of 'az iot central app monitor-events' * 'az iot central app device run-command' moved to 'az iot central device command run' * 'az iot central app device show-command-history' moved to 'az iot central device command history' * 'az iot central device twin' added to support deprecation of 'az iot central app device-twin' command group +IoT Hub updates + Cloud-to-Device message enhancements * Introduced new `az iot device c2d-message purge` command to purge the message queue for a device. @@ -42,6 +45,10 @@ Edge device creation enhancements * Enabled x509 self-signed certificate authentication types (`x509_thumbprint` and `x509_ca`) for edge device creation with `az iot hub device-identity create --ee` +Bug fixes + +* Fixes issue #243 where providing a connection string via --login still required "az login". + Digital Twins updates * The 'az dt route | model | twin' command groups support passing in a DT instance hostname directly. diff --git a/azext_iot/_help.py b/azext_iot/_help.py index a0614c334..f26ec7fef 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -1241,19 +1241,12 @@ helps[ "iot dps compute-device-key" -] = """ - type: group - short-summary: Generate a derived device SAS key -""" - -helps[ - "iot dps compute-device-key create" ] = """ type: command short-summary: Generate a derived device SAS key. - Long-summary: Generate a derived device key from a DPS enrollment group SAS token. + long-summary: Generate a derived device key from a DPS enrollment group symmetric key. examples: - - name: basic usage + - name: Basic usage text: > - az iot dps compute-device-key create --pk {primaryKey} --registration-id {registrationId} + az iot dps compute-device-key --key {enrollement_group_symmetric_key} --registration-id {registration_id} """ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index a714d63a2..698bb8cec 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -27,7 +27,6 @@ ReprovisionType, AllocationType, DistributedTracingSamplingModeType, - ModelSourceType, JobType, JobCreateType, JobStatusType, @@ -814,11 +813,11 @@ def load_arguments(self, _): with self.argument_context("iot dps compute-device-key") as context: context.argument( - "primary_key", - options_list=["--primary-key", "--pk"], - help="The primary symmetric shared access key stored in base64 format. ", + "symmetric_key", + options_list=["--symmetric-key", "--key"], + help="The symmetric shared access key for the enrollment group. ", ) - context.argument("registration_id", help="ID of device registration") + context.argument("registration_id", help="ID of device registration. ") with self.argument_context("iot dps enrollment") as context: context.argument("enrollment_id", help="ID of device enrollment record") @@ -911,49 +910,3 @@ def load_arguments(self, _): with self.argument_context("iot dps registration list") as context: context.argument("enrollment_id", help="ID of enrollment group") - - with self.argument_context("iot dt") as context: - context.argument( - "repo_login", - options_list=["--repo-login", "--rl"], - help="This command supports an entity connection string with rights to perform action. " - "Use to avoid PnP endpoint and repository name if repository is private. " - "If both an entity connection string and name are provided the connection string takes priority.", - ) - context.argument( - "interface", - options_list=["--interface", "-i"], - help="Target interface name. This should be the name of the interface not the urn-id.", - ) - context.argument( - "command_name", - options_list=["--command-name", "--cn"], - help="IoT Plug and Play interface command name.", - ) - context.argument( - "command_payload", - options_list=["--command-payload", "--cp", "--cv"], - help="IoT Plug and Play interface command payload. " - "Content can be directly input or extracted from a file path.", - ) - context.argument( - "interface_payload", - options_list=["--interface-payload", "--ip", "--iv"], - help="IoT Plug and Play interface payload. " - "Content can be directly input or extracted from a file path.", - ) - context.argument( - "source_model", - options_list=["--source", "-s"], - help="Choose your option to get model definition from specified source. ", - arg_type=get_enum_type(ModelSourceType), - ) - context.argument( - "schema", - options_list=["--schema"], - help="Show interface with entity schema.", - ) - - with self.argument_context("iot dt monitor-events") as context: - context.argument("timeout", arg_type=event_timeout_type) - context.argument("properties", arg_type=event_msg_prop_type) diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index bbc056c01..242bd878d 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -134,21 +134,14 @@ def _load_central_devices_help(): def _load_central_compute_device_key(): helps[ "iot central device compute-device-key" - ] = """ - type: group - short-summary: Generate a derived device SAS key - """ - - helps[ - "iot central device compute-device-key create" ] = """ type: command short-summary: Generate a derived device SAS key. - Long-summary: Generate a derived device key from primary group SAS token. + long-summary: Generate a derived device key from a group-level SAS key. examples: - - name: basic usage + - name: Basic usage text: > - az iot central device compute-device-key create --pk {primaryKey} --device-id {deviceid} + az iot central device compute-device-key --pk {primaryKey} --device-id {deviceid} """ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index 1048229a3..9c5579616 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -64,7 +64,7 @@ def load_central_commands(self, _): ) as cmd_group: cmd_group.command("create", "add_user") cmd_group.command("list", "list_users") - cmd_group.command("show", "get_user") + cmd_group.show_command("show", "get_user") cmd_group.command("delete", "delete_user") with self.command_group( @@ -72,18 +72,19 @@ def load_central_commands(self, _): ) as cmd_group: cmd_group.command("create", "add_api_token") cmd_group.command("list", "list_api_tokens") - cmd_group.command("show", "get_api_token") + cmd_group.show_command("show", "get_api_token") cmd_group.command("delete", "delete_api_token") with self.command_group( "iot central device", command_type=central_device_ops, is_preview=True, ) as cmd_group: # cmd_group.command("list", "list_devices") - cmd_group.command("show", "get_device") + cmd_group.show_command("show", "get_device") cmd_group.command("create", "create_device") cmd_group.command("delete", "delete_device") cmd_group.command("registration-info", "registration_info") cmd_group.command("show-credentials", "get_credentials") + cmd_group.command("compute-device-key", "compute_device_key") with self.command_group( "iot central device command", command_type=central_device_ops, is_preview=True, @@ -91,13 +92,6 @@ def load_central_commands(self, _): cmd_group.command("run", "run_command") cmd_group.command("history", "get_command_history") - with self.command_group( - "iot central device compute-device-key", - command_type=central_device_ops, - is_preview=True, - ) as cmd_group: - cmd_group.command("create", "compute_device_key") - with self.command_group( "iot central device-template", command_type=central_device_templates_ops, @@ -105,14 +99,14 @@ def load_central_commands(self, _): ) as cmd_group: # cmd_group.command("list", "list_device_templates") # cmd_group.command("map", "map_device_templates") - cmd_group.command("show", "get_device_template") + cmd_group.show_command("show", "get_device_template") cmd_group.command("create", "create_device_template") cmd_group.command("delete", "delete_device_template") with self.command_group( "iot central device twin", command_type=central_device_twin_ops, is_preview=True ) as cmd_group: - cmd_group.command( + cmd_group.show_command( "show", "device_twin_show", ) @@ -126,7 +120,7 @@ def _load_central_deprecated_commands(self, _): command_type=central_device_twin_ops, deprecate_info=self.deprecate(redirect="iot central device twin"), ) as cmd_group: - cmd_group.command( + cmd_group.show_command( "show", "device_twin_show", ) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 0b72cb3df..50c12608d 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -30,7 +30,7 @@ def load_command_table(self, _): "iot hub device-identity", command_type=iothub_ops ) as cmd_group: cmd_group.command("create", "iot_device_create") - cmd_group.command("show", "iot_device_show") + cmd_group.show_command("show", "iot_device_show") cmd_group.command("list", "iot_device_list") cmd_group.command("delete", "iot_device_delete") cmd_group.generic_update_command( @@ -55,13 +55,13 @@ def load_command_table(self, _): with self.command_group( "iot hub device-identity connection-string", command_type=iothub_ops ) as cmd_group: - cmd_group.command("show", "iot_get_device_connection_string") + cmd_group.show_command("show", "iot_get_device_connection_string") with self.command_group( "iot hub module-identity", command_type=iothub_ops ) as cmd_group: cmd_group.command("create", "iot_device_module_create") - cmd_group.command("show", "iot_device_module_show") + cmd_group.show_command("show", "iot_device_module_show") cmd_group.command("list", "iot_device_module_list") cmd_group.command("delete", "iot_device_module_delete") cmd_group.generic_update_command( @@ -70,7 +70,7 @@ def load_command_table(self, _): setter_name="iot_device_module_update", ) - cmd_group.command( + cmd_group.show_command( "show-connection-string", "iot_get_module_connection_string", deprecate_info=self.deprecate( @@ -81,12 +81,12 @@ def load_command_table(self, _): with self.command_group( "iot hub module-identity connection-string", command_type=iothub_ops ) as cmd_group: - cmd_group.command("show", "iot_get_module_connection_string") + cmd_group.show_command("show", "iot_get_module_connection_string") with self.command_group( "iot hub module-twin", command_type=iothub_ops ) as cmd_group: - cmd_group.command("show", "iot_device_module_twin_show") + cmd_group.show_command("show", "iot_device_module_twin_show") cmd_group.command("replace", "iot_device_module_twin_replace") cmd_group.generic_update_command( "update", @@ -99,7 +99,7 @@ def load_command_table(self, _): with self.command_group( "iot hub device-twin", command_type=iothub_ops ) as cmd_group: - cmd_group.command("show", "iot_device_twin_show") + cmd_group.show_command("show", "iot_device_twin_show") cmd_group.command("replace", "iot_device_twin_replace") cmd_group.generic_update_command( "update", @@ -114,7 +114,7 @@ def load_command_table(self, _): ) as cmd_group: cmd_group.command("show-metric", "iot_hub_configuration_metric_show") cmd_group.command("create", "iot_hub_configuration_create") - cmd_group.command("show", "iot_hub_configuration_show") + cmd_group.show_command("show", "iot_hub_configuration_show") cmd_group.command("list", "iot_hub_configuration_list") cmd_group.command("delete", "iot_hub_configuration_delete") cmd_group.generic_update_command( @@ -126,13 +126,13 @@ def load_command_table(self, _): with self.command_group( "iot hub distributed-tracing", command_type=iothub_ops, is_preview=True ) as cmd_group: - cmd_group.command("show", "iot_hub_distributed_tracing_show") + cmd_group.show_command("show", "iot_hub_distributed_tracing_show") cmd_group.command("update", "iot_hub_distributed_tracing_update") with self.command_group( "iot hub connection-string", command_type=iothub_ops ) as cmd_group: - cmd_group.command("show", "iot_hub_connection_string_show") + cmd_group.show_command("show", "iot_hub_connection_string_show") with self.command_group("iot edge", command_type=iothub_ops) as cmd_group: cmd_group.command("set-modules", "iot_edge_set_modules") @@ -142,7 +142,7 @@ def load_command_table(self, _): ) as cmd_group: cmd_group.command("show-metric", "iot_edge_deployment_metric_show") cmd_group.command("create", "iot_edge_deployment_create") - cmd_group.command("show", "iot_hub_configuration_show") + cmd_group.show_command("show", "iot_hub_configuration_show") cmd_group.command("list", "iot_edge_deployment_list") cmd_group.command("delete", "iot_hub_configuration_delete") cmd_group.generic_update_command( @@ -166,10 +166,13 @@ def load_command_table(self, _): cmd_group.command("send", "iot_c2d_message_send") cmd_group.command("purge", "iot_c2d_message_purge") + with self.command_group("iot dps", command_type=iotdps_ops) as cmd_group: + cmd_group.command("compute-device-key", "iot_dps_compute_device_key", is_preview=True) + with self.command_group("iot dps enrollment", command_type=iotdps_ops) as cmd_group: cmd_group.command("create", "iot_dps_device_enrollment_create") cmd_group.command("list", "iot_dps_device_enrollment_list") - cmd_group.command("show", "iot_dps_device_enrollment_get") + cmd_group.show_command("show", "iot_dps_device_enrollment_get") cmd_group.command("update", "iot_dps_device_enrollment_update") cmd_group.command("delete", "iot_dps_device_enrollment_delete") @@ -178,7 +181,7 @@ def load_command_table(self, _): ) as cmd_group: cmd_group.command("create", "iot_dps_device_enrollment_group_create") cmd_group.command("list", "iot_dps_device_enrollment_group_list") - cmd_group.command("show", "iot_dps_device_enrollment_group_get") + cmd_group.show_command("show", "iot_dps_device_enrollment_group_get") cmd_group.command("update", "iot_dps_device_enrollment_group_update") cmd_group.command("delete", "iot_dps_device_enrollment_group_delete") @@ -186,10 +189,5 @@ def load_command_table(self, _): "iot dps registration", command_type=iotdps_ops ) as cmd_group: cmd_group.command("list", "iot_dps_registration_list") - cmd_group.command("show", "iot_dps_registration_get") + cmd_group.show_command("show", "iot_dps_registration_get") cmd_group.command("delete", "iot_dps_registration_delete") - - with self.command_group( - "iot dps compute-device-key", command_type=iotdps_ops, is_preview=True - ) as cmd_group: - cmd_group.command("create", "iot_dps_compute_device_key") diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index b2659f4e9..c27e902f1 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -153,16 +153,6 @@ class PnPModelType(Enum): capabilityModel = "capabilityModel" -class ModelSourceType(Enum): - """ - Type of source to get model definition. - """ - - public = "public" - private = "private" - device = "device" - - class ConfigType(Enum): """ Type of configuration deployment. diff --git a/azext_iot/digitaltwins/command_map.py b/azext_iot/digitaltwins/command_map.py index ced47d90f..99722f0f3 100644 --- a/azext_iot/digitaltwins/command_map.py +++ b/azext_iot/digitaltwins/command_map.py @@ -43,14 +43,14 @@ def load_digitaltwins_commands(self, _): is_preview=True, ) as cmd_group: cmd_group.command("create", "create_instance") - cmd_group.command("show", "show_instance") + cmd_group.show_command("show", "show_instance") cmd_group.command("list", "list_instances") cmd_group.command("delete", "delete_instance") with self.command_group( "dt endpoint", command_type=digitaltwins_resource_ops ) as cmd_group: - cmd_group.command( + cmd_group.show_command( "show", "show_endpoint", table_transformer=( @@ -78,7 +78,7 @@ def load_digitaltwins_commands(self, _): with self.command_group( "dt route", command_type=digitaltwins_route_ops ) as cmd_group: - cmd_group.command( + cmd_group.show_command( "show", "show_route", table_transformer="{RouteName:id,EndpointName:endpointName,Filter:filter}", @@ -101,21 +101,21 @@ def load_digitaltwins_commands(self, _): with self.command_group("dt twin", command_type=digitaltwins_twin_ops) as cmd_group: cmd_group.command("query", "query_twins") cmd_group.command("create", "create_twin") - cmd_group.command("show", "show_twin") + cmd_group.show_command("show", "show_twin") cmd_group.command("update", "update_twin") cmd_group.command("delete", "delete_twin") with self.command_group( "dt twin component", command_type=digitaltwins_twin_ops ) as cmd_group: - cmd_group.command("show", "show_component") + cmd_group.show_command("show", "show_component") cmd_group.command("update", "update_component") with self.command_group( "dt twin relationship", command_type=digitaltwins_twin_ops ) as cmd_group: cmd_group.command("create", "create_relationship") - cmd_group.command("show", "show_relationship") + cmd_group.show_command("show", "show_relationship") cmd_group.command("list", "list_relationships") cmd_group.command("update", "update_relationship") cmd_group.command("delete", "delete_relationship") @@ -129,7 +129,7 @@ def load_digitaltwins_commands(self, _): "dt model", command_type=digitaltwins_model_ops ) as cmd_group: cmd_group.command("create", "add_models") - cmd_group.command( + cmd_group.show_command( "show", "show_model", table_transformer="{ModelId:id,UploadTime:uploadTime,Decommissioned:decommissioned}", diff --git a/azext_iot/iothub/command_map.py b/azext_iot/iothub/command_map.py index a100d797e..e640fcb20 100644 --- a/azext_iot/iothub/command_map.py +++ b/azext_iot/iothub/command_map.py @@ -21,11 +21,11 @@ def load_iothub_commands(self, _): """ with self.command_group("iot hub job", command_type=iothub_job_ops) as cmd_group: cmd_group.command("create", "job_create") - cmd_group.command("show", "job_show") + cmd_group.show_command("show", "job_show") cmd_group.command("list", "job_list") cmd_group.command("cancel", "job_cancel") with self.command_group("iot pnp twin", command_type=pnp_runtime_ops, is_preview=True) as cmd_group: cmd_group.command("invoke-command", "invoke_device_command") - cmd_group.command("show", "get_digital_twin") + cmd_group.show_command("show", "get_digital_twin") cmd_group.command("update", "patch_digital_twin") diff --git a/azext_iot/iothub/providers/discovery.py b/azext_iot/iothub/providers/discovery.py index 296a8c526..34069401f 100644 --- a/azext_iot/iothub/providers/discovery.py +++ b/azext_iot/iothub/providers/discovery.py @@ -25,7 +25,6 @@ def __init__(self, cmd): self.cmd = cmd self.client = None self.sub_id = "unknown" - self._initialize_client() def _initialize_client(self): if not self.client: @@ -36,6 +35,8 @@ def _initialize_client(self): self.sub_id = self.client.config.subscription_id def get_iothubs(self, rg: str = None) -> List: + self._initialize_client() + hubs_list = [] if not rg: @@ -52,6 +53,8 @@ def get_iothubs(self, rg: str = None) -> List: return hubs_list def get_policies(self, hub_name: str, rg: str) -> List: + self._initialize_client() + policy_pager = self.client.list_keys( resource_group_name=rg, resource_name=hub_name ) @@ -66,6 +69,8 @@ def get_policies(self, hub_name: str, rg: str) -> List: return policy_list def find_iothub(self, hub_name: str, rg: str = None): + self._initialize_client() + from azure.mgmt.iothub.models import ErrorDetailsException if rg: @@ -94,6 +99,8 @@ def find_iothub(self, hub_name: str, rg: str = None): ) def find_policy(self, hub_name: str, rg: str, policy_name: str = "auto"): + self._initialize_client() + if policy_name.lower() != "auto": return self.client.get_keys_for_key_name( resource_group_name=rg, resource_name=hub_name, key_name=policy_name diff --git a/azext_iot/operations/dps.py b/azext_iot/operations/dps.py index 649aaefbe..b4cffe73a 100644 --- a/azext_iot/operations/dps.py +++ b/azext_iot/operations/dps.py @@ -544,9 +544,9 @@ def iot_dps_device_enrollment_group_delete( def iot_dps_compute_device_key( - cmd, primary_key, registration_id, + cmd, symmetric_key, registration_id, ): - return compute_device_key(primary_key=primary_key, registration_id=registration_id) + return compute_device_key(primary_key=symmetric_key, registration_id=registration_id) # DPS Registration diff --git a/azext_iot/pnp/command_map.py b/azext_iot/pnp/command_map.py index c35ad94c8..f3d6a35d2 100644 --- a/azext_iot/pnp/command_map.py +++ b/azext_iot/pnp/command_map.py @@ -33,7 +33,7 @@ def load_pnp_commands(self, _): with self.command_group( "iot pnp model", command_type=pnp_model_ops, is_preview=True ) as cmd_group: - cmd_group.command("show", "iot_pnp_model_show") + cmd_group.show_command("show", "iot_pnp_model_show") cmd_group.command("create", "iot_pnp_model_create") cmd_group.command("publish", "iot_pnp_model_publish", confirmation=True) cmd_group.command("list", "iot_pnp_model_list") diff --git a/azext_iot/product/test/command_map.py b/azext_iot/product/test/command_map.py index 7dcb0557e..a6f9f457f 100644 --- a/azext_iot/product/test/command_map.py +++ b/azext_iot/product/test/command_map.py @@ -26,7 +26,7 @@ def load_product_test_commands(self, _): with self.command_group("iot product test", command_type=tests_ops) as g: g.command("create", "create") g.command("update", "update") - g.command("show", "show") + g.show_command("show", "show") g.command("search", "search") with self.command_group("iot product test case", command_type=test_cases_ops) as g: g.command("list", "list") @@ -34,7 +34,7 @@ def load_product_test_commands(self, _): with self.command_group("iot product test task", command_type=test_tasks_ops) as g: g.command("create", "create") g.command("delete", "delete") - g.command("show", "show") + g.show_command("show", "show") with self.command_group("iot product test run", command_type=test_runs_ops) as g: - g.command("show", "show") + g.show_command("show", "show") g.command("submit", "submit") diff --git a/azext_iot/tests/iothub/test_iothub_discovery_unit.py b/azext_iot/tests/iothub/test_iothub_discovery_unit.py new file mode 100644 index 000000000..0466f546e --- /dev/null +++ b/azext_iot/tests/iothub/test_iothub_discovery_unit.py @@ -0,0 +1,58 @@ +# 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.iothub.providers.discovery import IotHubDiscovery +from azext_iot.common._azure import parse_iot_hub_connection_string + + +@pytest.fixture +def get_mgmt_client(mocker, fixture_cmd): + 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"), + ) + patch = mocker.patch("azext_iot._factory.iot_hub_service_factory") + patch.return_value = None + + return patch + + +class TestIoTHubDiscovery: + def test_get_target_by_cstring(self, fixture_cmd, get_mgmt_client): + discovery = IotHubDiscovery(cmd=fixture_cmd) + + fake_login = ( + "HostName=CoolIoTHub.azure-devices.net;SharedAccessKeyName=iothubowner;" + "SharedAccessKey=AB+c/+5nm2XpDXcffhnGhnxz/TVF4m5ag7AuVIGwchj=" + ) + parsed_fake_login = parse_iot_hub_connection_string(fake_login) + + target = discovery.get_target( + hub_name=None, resource_group_name=None, login=fake_login + ) + + # Ensure no ARM calls are made + assert get_mgmt_client.call_count == 0 + + assert target["cs"] == fake_login + assert target["entity"] == parsed_fake_login["HostName"] + assert target["policy"] == parsed_fake_login["SharedAccessKeyName"] + assert target["primarykey"] == parsed_fake_login["SharedAccessKey"] + + target = discovery.get_target_by_cstring(fake_login) + + # Ensure no ARM calls are made + assert get_mgmt_client.call_count == 0 + + assert target["cs"] == fake_login + assert target["entity"] == parsed_fake_login["HostName"] + assert target["policy"] == parsed_fake_login["SharedAccessKeyName"] + assert target["primarykey"] == parsed_fake_login["SharedAccessKey"] diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 930594712..6f17cd28f 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -197,7 +197,7 @@ def test_device_connect(self): device_id = "testDevice" device_primary_key = self.cmd( - "iot central device compute-device-key create --pk {} -d {}".format( + "iot central device compute-device-key --pk {} -d {}".format( APP_PRIMARY_KEY, device_id ), ).get_output_in_json() diff --git a/azext_iot/tests/test_iot_dps_int.py b/azext_iot/tests/test_iot_dps_int.py index 3ea92dccd..b3eb92174 100644 --- a/azext_iot/tests/test_iot_dps_int.py +++ b/azext_iot/tests/test_iot_dps_int.py @@ -20,6 +20,11 @@ cert_name = 'test' cert_path = cert_name + '-cert.pem' +test_endorsement_key = ('AToAAQALAAMAsgAgg3GXZ0SEs/gakMyNRqXXJP1S124GUgtk8qHaGzMUaaoABgCAAEMAEAgAAAAAAAEAibym9HQP9vxCGF5dVc1Q' + 'QsAGe021aUGJzNol1/gycBx3jFsTpwmWbISRwnFvflWd0w2Mc44FAAZNaJOAAxwZvG8GvyLlHh6fGKdh+mSBL4iLH2bZ4Ry22cB3' + 'CJVjXmdGoz9Y/j3/NwLndBxQC+baNvzvyVQZ4/A2YL7vzIIj2ik4y+ve9ir7U0GbNdnxskqK1KFIITVVtkTIYyyFTIR0BySjPrRI' + 'Dj7r7Mh5uF9HBppGKQCBoVSVV8dI91lNazmSdpGWyqCkO7iM4VvUMv2HT/ym53aYlUrau+Qq87Tu+uQipWYgRdF11KDfcpMHqqzB' + 'QQ1NpOJVhrsTrhyJzO7KNw==') class IoTDpsTest(LiveScenarioTest): @@ -28,7 +33,7 @@ class IoTDpsTest(LiveScenarioTest): provisioning_status_new = EntityStatusType.disabled.value def __init__(self, test_method): - super(IoTDpsTest, self).__init__('test_dps_enrollment_tpm_lifecycle') + super(IoTDpsTest, self).__init__(test_method) output_dir = os.getcwd() create_self_signed_certificate(cert_name, 200, output_dir, True) self.kwargs['generic_dict'] = {'count': None, 'key': 'value', 'metadata': None, 'version': None} @@ -37,13 +42,15 @@ def __del__(self): if os.path.exists(cert_path): os.remove(cert_path) + def test_dps_compute_device_key(self): + device_key = self.cmd('az iot dps compute-device-key --key "{}" ' + '--registration-id myarbitrarydeviceId'.format(test_endorsement_key)).output + device_key = device_key.strip("\"'\n") + assert device_key == "cT/EXZvsplPEpT//p98Pc6sKh8mY3kYgSxavHwMkl7w=" + def test_dps_enrollment_tpm_lifecycle(self): enrollment_id = self.create_random_name('enrollment-for-test', length=48) - endorsement_key = ('AToAAQALAAMAsgAgg3GXZ0SEs/gakMyNRqXXJP1S124GUgtk8qHaGzMUaaoABgCAAEMAEAgAAAAAAAEAibym9HQP9vxCGF5dVc1Q' - 'QsAGe021aUGJzNol1/gycBx3jFsTpwmWbISRwnFvflWd0w2Mc44FAAZNaJOAAxwZvG8GvyLlHh6fGKdh+mSBL4iLH2bZ4Ry22cB3' - 'CJVjXmdGoz9Y/j3/NwLndBxQC+baNvzvyVQZ4/A2YL7vzIIj2ik4y+ve9ir7U0GbNdnxskqK1KFIITVVtkTIYyyFTIR0BySjPrRI' - 'Dj7r7Mh5uF9HBppGKQCBoVSVV8dI91lNazmSdpGWyqCkO7iM4VvUMv2HT/ym53aYlUrau+Qq87Tu+uQipWYgRdF11KDfcpMHqqzB' - 'QQ1NpOJVhrsTrhyJzO7KNw==') + endorsement_key = test_endorsement_key device_id = self.create_random_name('device-id-for-test', length=48) attestation_type = AttestationType.tpm.value hub_host_name = '{}.azure-devices.net'.format(hub) @@ -225,7 +232,7 @@ def test_dps_enrollment_symmetrickey_lifecycle(self): self.check('allocationPolicy', "custom"), self.check('customAllocationDefinition.webhookUrl', webhook_url), self.check('customAllocationDefinition.apiVersion', api_version), - self.check('iotHubs', hub_host_name.split()), + # self.check('iotHubs', hub_host_name.split()), TODO self.exists('initialTwin.tags'), self.exists('initialTwin.properties.desired'), self.check('attestation.symmetricKey.primaryKey', primary_key), diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index f1413778b..19f8b3441 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -373,4 +373,4 @@ def _validate_directory(path): invalid_directories.append("Directory: '{}' missing __init__.py".format(directory)) if invalid_directories: - pytest.fail(", ".join(directory)) + pytest.fail(", ".join(invalid_directories)) From e493cb0805e3fabd611a1837eeca5331ce6bcefa Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 27 Aug 2020 14:07:07 -0700 Subject: [PATCH 104/179] Update HISTORY.rst --- HISTORY.rst | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 379ae5532..0127b7937 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,32 +6,34 @@ Release History 0.9.9 +++++++++++++++ -IoT DPS updates +**IoT DPS updates** * Introduces 'az iot dps compute-device-key' preview command to generate derived device SAS key -IoT Central updates +**IoT Central updates** -* Current release involves re-grouping of IoT central commands. - - * 'az iot central app device-twin' deprecated use 'az iot central device twin' instead. Deprecated command group is planned to be removed by December 2020 - * 'az iot central app monitor-events' deprecated use 'az iot central diagnostics monitor-events' instead. Deprecated command is planned to be removed by December 2020 - * Note all commands listed below are in preview +* Introduces 'az iot central diagnostics' preview command group to perform application and device level diagnostics +* Introduces 'az iot central device compute-device-key' preview command to generate derived device SAS key - * Introduces 'az iot central diagnostics' preview command group to perform application and device level diagnostics - * Introduces 'az iot central device compute-device-key' preview command to generate derived device SAS key +* This release involves a re-grouping of IoT Central commands. + + Set of changes for GA commands + + * 'az iot central app device-twin' is deprecated use 'az iot central device twin' instead. Deprecated command group is planned to be removed by December 2020 + * 'az iot central app monitor-events' is deprecated use 'az iot central diagnostics monitor-events' instead. Deprecated command is planned to be removed by December 2020 - * 'az iot central app device registration-summary' moved to 'az iot central diagnostics registration-summary' - * 'az iot central app monitor-properties' moved to 'az iot central diagnostics monitor-properties' - * 'az iot central app validate-messages' moved to 'az iot central diagnostics validate-messages' - * 'az iot central app validate-properties' moved to 'az iot central diagnostics validate-properties' - * 'az iot central diagnostics monitor-events' added to support deprecation of 'az iot central app monitor-events' + Set of changes for preview commands - * 'az iot central app device run-command' moved to 'az iot central device command run' - * 'az iot central app device show-command-history' moved to 'az iot central device command history' - * 'az iot central device twin' added to support deprecation of 'az iot central app device-twin' command group + * 'az iot central app device registration-summary' moved to 'az iot central diagnostics registration-summary' + * 'az iot central app monitor-properties' moved to 'az iot central diagnostics monitor-properties' + * 'az iot central app validate-messages' moved to 'az iot central diagnostics validate-messages' + * 'az iot central app validate-properties' moved to 'az iot central diagnostics validate-properties' + * 'az iot central diagnostics monitor-events' added to support deprecation of 'az iot central app monitor-events' + * 'az iot central app device run-command' moved to 'az iot central device command run' + * 'az iot central app device show-command-history' moved to 'az iot central device command history' + * 'az iot central device twin' added to support deprecation of 'az iot central app device-twin' command group -IoT Hub updates +**IoT Hub updates** Cloud-to-Device message enhancements @@ -43,18 +45,22 @@ Cloud-to-Device message enhancements Edge device creation enhancements -* Enabled x509 self-signed certificate authentication types (`x509_thumbprint` and `x509_ca`) for edge device creation with `az iot hub device-identity create --ee` +* Enabled x509 certificate authentication types (`x509_thumbprint` and `x509_ca`) for edge device creation with `az iot hub device-identity create --ee` Bug fixes * Fixes issue #243 where providing a connection string via --login still required "az login". -Digital Twins updates +**Digital Twins updates** + +The following command groups support passing in a DT instance hostname directly. -* The 'az dt route | model | twin' command groups support passing in a DT instance hostname directly. + * az dt route + * az dt model + * az dt twin - * If an instance name is provided, the user subscription is first queried for the target instance to retrieve the hostname. - * If a hostname is provided, the subscription query is skipped and the provided value is used for subsequent interaction. +* Like before, if an instance name is provided, the user subscription is first queried for the target instance to retrieve the hostname. +* If a hostname is provided, the subscription query is skipped and the provided value is used for subsequent interaction. 0.9.8 From 560b21ee54a2e0cf5958b192bbc43f9113fc9c7f Mon Sep 17 00:00:00 2001 From: valluriraj Date: Thu, 27 Aug 2020 14:20:44 -0700 Subject: [PATCH 105/179] Read me update (#245) --- azext_iot/central/_help.py | 2 +- azext_iot/commands.py | 4 +++- azext_iot/operations/dps.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 242bd878d..f73335302 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -21,7 +21,7 @@ def load_central_help(): IoT infrastructure. IoT Central documentation is available at https://aka.ms/iotcentral-documentation - For more information on command usage, see: aka.ms/azure-cli-iot-ext + Additional information on CLI commands is available at https://aka.ms/azure-cli-iot-ext """ helps[ diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 50c12608d..1085b1fd6 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -167,7 +167,9 @@ def load_command_table(self, _): cmd_group.command("purge", "iot_c2d_message_purge") with self.command_group("iot dps", command_type=iotdps_ops) as cmd_group: - cmd_group.command("compute-device-key", "iot_dps_compute_device_key", is_preview=True) + cmd_group.command( + "compute-device-key", "iot_dps_compute_device_key", is_preview=True + ) with self.command_group("iot dps enrollment", command_type=iotdps_ops) as cmd_group: cmd_group.command("create", "iot_dps_device_enrollment_create") diff --git a/azext_iot/operations/dps.py b/azext_iot/operations/dps.py index b4cffe73a..b46ef951d 100644 --- a/azext_iot/operations/dps.py +++ b/azext_iot/operations/dps.py @@ -546,7 +546,9 @@ def iot_dps_device_enrollment_group_delete( def iot_dps_compute_device_key( cmd, symmetric_key, registration_id, ): - return compute_device_key(primary_key=symmetric_key, registration_id=registration_id) + return compute_device_key( + primary_key=symmetric_key, registration_id=registration_id + ) # DPS Registration From 8e35b887e778c5c1cead3a526e4b4fb7d9911705 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 27 Aug 2020 15:10:48 -0700 Subject: [PATCH 106/179] Update ADT data plane cmd instance var name in help. (#246) --- azext_iot/digitaltwins/_help.py | 90 ++++++++++++++++----------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index 0f4dc7b1a..fc029de3a 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -240,10 +240,10 @@ def load_digitaltwins_help(): examples: - name: Adds an event route for an existing endpoint on target instance with default filter of "true". text: > - az dt route create -n {instance_name} --endpoint-name {endpoint_name} --route-name {route_name} + az dt route create -n {instance_or_hostname} --endpoint-name {endpoint_name} --route-name {route_name} - name: Adds an event route for an existing endpoint on target instance with custom filter. text: > - az dt route create -n {instance_name} --endpoint-name {endpoint_name} --route-name {route_name} + az dt route create -n {instance_or_hostname} --endpoint-name {endpoint_name} --route-name {route_name} --filter "type = 'Microsoft.DigitalTwins.Twin.Create'" """ @@ -254,7 +254,7 @@ def load_digitaltwins_help(): examples: - name: List configured event routes of a target instance. text: > - az dt route list -n {instance_name} + az dt route list -n {instance_or_hostname} """ helps["dt route delete"] = """ @@ -264,7 +264,7 @@ def load_digitaltwins_help(): examples: - name: Remove an event route from a target instance. text: > - az dt route delete -n {instance_name} --route-name {route_name} + az dt route delete -n {instance_or_hostname} --route-name {route_name} """ helps["dt route show"] = """ @@ -274,7 +274,7 @@ def load_digitaltwins_help(): examples: - name: Show an event route on a target instance. text: > - az dt route show -n {instance_name} --route-name {route_name} + az dt route show -n {instance_or_hostname} --route-name {route_name} """ helps["dt twin"] = """ @@ -290,17 +290,17 @@ def load_digitaltwins_help(): examples: - name: Create a digital twin from an existing (prior-created) model. text: > - az dt twin create -n {instance_name} --dtmi dtmi:example:Room;1 + az dt twin create -n {instance_or_hostname} --dtmi dtmi:example:Room;1 --twin-id {twin_id} - name: Create a digital twin from an existing (prior-created) model. Instantiate with property values. text: > - az dt twin create -n {instance_name} --dtmi dtmi:com:example:DeviceInformation;1 + az dt twin create -n {instance_or_hostname} --dtmi dtmi:com:example:DeviceInformation;1 --twin-id {twin_id} --properties '{"manufacturer": "Microsoft"}' - name: Create a digital twin with component from existing (prior-created) models. Instantiate with property values. text: > - az dt twin create -n {instance_name} --dtmi dtmi:com:example:TemperatureController;1 --twin-id {twin_id} --properties '{ + az dt twin create -n {instance_or_hostname} --dtmi dtmi:com:example:TemperatureController;1 --twin-id {twin_id} --properties '{ "Temperature": 10.2, "Thermostat": { "$metadata": {}, @@ -318,12 +318,12 @@ def load_digitaltwins_help(): examples: - name: Update a digital twin via JSON patch specification. text: > - az dt twin update -n {instance_name} --twin-id {twin_id} + az dt twin update -n {instance_or_hostname} --twin-id {twin_id} --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' - name: Update a digital twin via JSON patch specification. text: > - az dt twin update -n {instance_name} --twin-id {twin_id} + az dt twin update -n {instance_or_hostname} --twin-id {twin_id} --json-patch '[ {"op":"replace", "path":"/Temperature", "value": 20.5}, {"op":"add", "path":"/Areas", "value": ["ControlSystem"]} @@ -331,7 +331,7 @@ def load_digitaltwins_help(): - name: Update a digital twin via JSON patch specification defined in a file. text: > - az dt twin update -n {instance_name} --twin-id {twin_id} + az dt twin update -n {instance_or_hostname} --twin-id {twin_id} --json-patch ./my/patch/document.json """ @@ -342,7 +342,7 @@ def load_digitaltwins_help(): examples: - name: Show the details of a digital twin. text: > - az dt twin show -n {instance_name} --twin-id {twin_id} + az dt twin show -n {instance_or_hostname} --twin-id {twin_id} """ helps["dt twin query"] = """ @@ -352,11 +352,11 @@ def load_digitaltwins_help(): examples: - name: Query all digital twins in target instance and project all attributes. Also show cost in query units. text: > - az dt twin query -n {instance_name} -q "select * from digitaltwins" --show-cost + az dt twin query -n {instance_or_hostname} -q "select * from digitaltwins" --show-cost - name: Query by model and project all attributes. text: > - az dt twin query -n {instance_name} -q "select * from digitaltwins T where IS_OF_MODEL(T, 'dtmi:example:Room;2')" + az dt twin query -n {instance_or_hostname} -q "select * from digitaltwins T where IS_OF_MODEL(T, 'dtmi:example:Room;2')" """ helps["dt twin delete"] = """ @@ -366,7 +366,7 @@ def load_digitaltwins_help(): examples: - name: Remove a digital twin by Id. text: > - az dt twin delete -n {instance_name} --twin-id {twin_id} + az dt twin delete -n {instance_or_hostname} --twin-id {twin_id} """ helps["dt twin relationship"] = """ @@ -382,12 +382,12 @@ def load_digitaltwins_help(): examples: - name: Create a relationship between two digital twins. text: > - az dt twin relationship create -n {instance_name} --relationship-id {relationship_id} --relationship contains + az dt twin relationship create -n {instance_or_hostname} --relationship-id {relationship_id} --relationship contains --twin-id {source_twin_id} --target {target_twin_id} - name: Create a relationship with initialized properties between two digital twins. text: > - az dt twin relationship create -n {instance_name} --relationship-id {relationship_id} --relationship contains + az dt twin relationship create -n {instance_or_hostname} --relationship-id {relationship_id} --relationship contains --twin-id {source_twin_id} --target {target_twin_id} --properties '{"ownershipUser": "me", "ownershipDepartment": "Computer Science"}' """ @@ -399,7 +399,7 @@ def load_digitaltwins_help(): examples: - name: Show details of a digital twin relationship. text: > - az dt twin relationship show -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + az dt twin relationship show -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} """ helps["dt twin relationship list"] = """ @@ -409,19 +409,19 @@ def load_digitaltwins_help(): examples: - name: List outgoing relationships of a digital twin. text: > - az dt twin relationship list -n {instance_name} --twin-id {twin_id} + az dt twin relationship list -n {instance_or_hostname} --twin-id {twin_id} - name: List outgoing relationships of a digital twin and filter on relationship 'contains' text: > - az dt twin relationship list -n {instance_name} --twin-id {twin_id} --relationship contains + az dt twin relationship list -n {instance_or_hostname} --twin-id {twin_id} --relationship contains - name: List incoming relationships of a digital twin. text: > - az dt twin relationship list -n {instance_name} --twin-id {twin_id} --incoming + az dt twin relationship list -n {instance_or_hostname} --twin-id {twin_id} --incoming - name: List incoming relationships of a digital twin and filter on relationship 'contains'. text: > - az dt twin relationship list -n {instance_name} --twin-id {twin_id} --relationship contains --incoming + az dt twin relationship list -n {instance_or_hostname} --twin-id {twin_id} --relationship contains --incoming """ helps["dt twin relationship update"] = """ @@ -433,12 +433,12 @@ def load_digitaltwins_help(): examples: - name: Update a digital twin relationship via JSON patch specification. text: > - az dt twin relationship update -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + az dt twin relationship update -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} --relationship contains --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' - name: Update a digital twin relationship via JSON patch specification. text: > - az dt twin relationship update -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + az dt twin relationship update -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} --relationship contains --json-patch '[ {"op":"replace", "path":"/Temperature", "value": 20.5}, {"op":"add", "path":"/Areas", "value": ["ControlSystem"]} @@ -446,7 +446,7 @@ def load_digitaltwins_help(): - name: Update a digital twin relationship via JSON patch specification defined in a file. text: > - az dt twin relationship update -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + az dt twin relationship update -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} --relationship contains --json-patch ./my/patch/document.json """ @@ -457,7 +457,7 @@ def load_digitaltwins_help(): examples: - name: Delete a digital twin relationship. text: > - az dt twin relationship delete -n {instance_name} --twin-id {twin_id} --relationship-id {relationship_id} + az dt twin relationship delete -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} """ helps["dt twin telemetry"] = """ @@ -473,12 +473,7 @@ def load_digitaltwins_help(): examples: - name: Send twin telemetry text: > - az dt twin telemetry send -n {instance_name} --twin-id {twin_id} - """ - - helps["dt model"] = """ - type: group - short-summary: Manage DTDL models and definitions on a Digital Twins instance. + az dt twin telemetry send -n {instance_or_hostname} --twin-id {twin_id} """ helps["dt twin component"] = """ @@ -493,7 +488,7 @@ def load_digitaltwins_help(): examples: - name: Show details of a digital twin component text: > - az dt twin component show -n {instance_name} --twin-id {twin_id} --component Thermostat + az dt twin component show -n {instance_or_hostname} --twin-id {twin_id} --component Thermostat """ helps["dt twin component update"] = """ @@ -505,12 +500,12 @@ def load_digitaltwins_help(): examples: - name: Update a digital twin component via JSON patch specification. text: > - az dt twin component update -n {instance_name} --twin-id {twin_id} --component {component_path} + az dt twin component update -n {instance_or_hostname} --twin-id {twin_id} --component {component_path} --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' - name: Update a digital twin component via JSON patch specification. text: > - az dt twin component update -n {instance_name} --twin-id {twin_id} --component {component_path} + az dt twin component update -n {instance_or_hostname} --twin-id {twin_id} --component {component_path} --json-patch '[ {"op":"replace", "path":"/Temperature", "value": 20.5}, {"op":"add", "path":"/Areas", "value": ["ControlSystem"]} @@ -518,10 +513,15 @@ def load_digitaltwins_help(): - name: Update a digital twin component via JSON patch specification defined in a file. text: > - az dt twin component update -n {instance_name} --twin-id {twin_id} --component {component_path} + az dt twin component update -n {instance_or_hostname} --twin-id {twin_id} --component {component_path} --json-patch ./my/patch/document.json """ + helps["dt model"] = """ + type: group + short-summary: Manage DTDL models and definitions on a Digital Twins instance. + """ + helps["dt model create"] = """ type: command short-summary: Uploads one or more models. When any error occurs, no models are uploaded. @@ -530,11 +530,11 @@ def load_digitaltwins_help(): examples: - name: Bulk upload all .json or .dtdl model files from a target directory. Model processing is recursive. text: > - az dt model create -n {instance_name} --from-directory {directory_path} + az dt model create -n {instance_or_hostname} --from-directory {directory_path} - name: Upload model json inline or from file path. text: > - az dt model create -n {instance_name} --models {file_path_or_inline_json} + az dt model create -n {instance_or_hostname} --models {file_path_or_inline_json} """ helps["dt model show"] = """ @@ -544,11 +544,11 @@ def load_digitaltwins_help(): examples: - name: Show model meta data text: > - az dt model show -n {instance_name} --dtmi "dtmi:example:Floor;1" + az dt model show -n {instance_or_hostname} --dtmi "dtmi:example:Floor;1" - name: Show model meta data and definition text: > - az dt model show -n {instance_name} --dtmi "dtmi:example:Floor;1" --definition + az dt model show -n {instance_or_hostname} --dtmi "dtmi:example:Floor;1" --definition """ helps["dt model list"] = """ @@ -558,15 +558,15 @@ def load_digitaltwins_help(): examples: - name: List model metadata text: > - az dt model list -n {instance_name} + az dt model list -n {instance_or_hostname} - name: List model definitions text: > - az dt model list -n {instance_name} --definition + az dt model list -n {instance_or_hostname} --definition - name: List dependencies of particular pre-existing model(s). Space seperate dtmi values. text: > - az dt model list -n {instance_name} --dependencies-for {model_id0} {model_id1} + az dt model list -n {instance_or_hostname} --dependencies-for {model_id0} {model_id1} """ helps["dt model update"] = """ @@ -576,7 +576,7 @@ def load_digitaltwins_help(): examples: - name: Decommision a target model text: > - az dt model update -n {instance_name} --dtmi "dtmi:example:Floor;1" --decommission + az dt model update -n {instance_or_hostname} --dtmi "dtmi:example:Floor;1" --decommission """ helps["dt model delete"] = """ @@ -586,5 +586,5 @@ def load_digitaltwins_help(): examples: - name: Delete a target model. text: > - az dt model delete -n {instance_name} --dtmi "dtmi:example:Floor;1" + az dt model delete -n {instance_or_hostname} --dtmi "dtmi:example:Floor;1" """ From 0162de95ed46c49ad751727c48819a47401756aa Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 27 Aug 2020 17:05:11 -0700 Subject: [PATCH 107/179] Update device export help. (#247) --- azext_iot/_help.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index f26ec7fef..7c0ce4185 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -241,14 +241,16 @@ "iot hub device-identity export" ] = """ type: command - short-summary: Export all device identities from an IoT Hub to an Azure Storage blob container. + short-summary: Export all device identities from an IoT Hub to an Azure Storage blob container. For inline + blob container SAS uri input, please review the input rules of your environment. long-summary: For more information, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities examples: - - name: Export all device identities to a configured blob container using an inline SAS uri. + - name: Export all device identities to a configured blob container and include device keys. Uses an inline SAS uri example. text: > - az iot hub device-identity export -n {iothub_name} --bcu {sas_uri} - - name: Export all device identities to a configured blob container using a file path which contains SAS uri. + az iot hub device-identity export -n {iothub_name} --ik --bcu + 'https://mystorageaccount.blob.core.windows.net/devices?sv=2019-02-02&st=2020-08-23T22%3A35%3A00Z&se=2020-08-24T22%3A35%3A00Z&sr=c&sp=rwd&sig=VrmJ5sQtW3kLzYg10VqmALGCp4vtYKSLNjZDDJBSh9s%3D' + - 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} """ @@ -257,7 +259,8 @@ "iot hub device-identity import" ] = """ type: command - short-summary: Import device identities to an IoT Hub from a blob. + short-summary: Import device identities to an IoT Hub from a blob. For inline + blob container SAS uri input, please review the input rules of your environment. long-summary: For more information, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities examples: From af9f64b82aa9a2bb1abc849e922a8c964d354b89 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 28 Aug 2020 13:16:02 -0700 Subject: [PATCH 108/179] Update create-release.yml for Azure Pipelines --- .azure-devops/create-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index d39db27bb..7ed3cfacb 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -46,7 +46,7 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.6.x' - runUnitTestsOnly: 'false' + runUnitTestsOnly: $[coalesce(variables['runUnitTestsOnly'], 'false')] - script: 'pip install .' displayName: 'Install the whl locally' From b6b6d46b59bb99321871d0efb4891e256ac7e847 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Fri, 28 Aug 2020 14:20:37 -0700 Subject: [PATCH 109/179] Remove universal wheel config. (#248) --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index d80e75ff3..7784f4010 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal=1 - [flake8] max-complexity = 10 max-line-length = 130 From 7f32e2dfdf2b058739a880f4f4c91a59eb913577 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Fri, 28 Aug 2020 14:23:45 -0700 Subject: [PATCH 110/179] Increment version. --- azext_iot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azext_iot/constants.py b/azext_iot/constants.py index bd01aeaf3..87f1fb11a 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.9.9" +VERSION = "0.10.0" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" From 558bc3169973be3c6946c4bbe8c21e94ac7928fb Mon Sep 17 00:00:00 2001 From: Paul Montgomery Date: Tue, 1 Sep 2020 08:48:17 -0700 Subject: [PATCH 111/179] Service might still have this queued under load. Command should still return data. (#249) --- azext_iot/tests/product/test_aics_e2e_int.py | 1 - 1 file changed, 1 deletion(-) diff --git a/azext_iot/tests/product/test_aics_e2e_int.py b/azext_iot/tests/product/test_aics_e2e_int.py index d91a1a114..a9fc51775 100644 --- a/azext_iot/tests/product/test_aics_e2e_int.py +++ b/azext_iot/tests/product/test_aics_e2e_int.py @@ -93,7 +93,6 @@ def test_e2e(self): test_task = self.cmd( "iot product test task show -t {device_test_id} --task-id {generate_task_id} --base-url {BASE_URL}" ).get_output_in_json() - assert test_task.get("status") == DeviceTestTaskStatus.completed.value assert test_task.get("error") is None assert test_task.get("type") == TaskType.GenerateTestCases.value From cfdc6a82f78076446d250b922c6273f5c52ee51e Mon Sep 17 00:00:00 2001 From: Ryan K Date: Fri, 11 Sep 2020 13:39:25 -0700 Subject: [PATCH 112/179] DPS updates (#251) * Regenerate 2019-03-31 DPS SDK * Support --show-keys argument to add attestation information to enrollment/enrollment-group show results * Simplified show/list registration ops to use raw result instead of model (removes extraneous null fields) * Updated DPS unit tests to use new mocked_response pattern * Integration test updates * Removed DPS device operations SDK until commands are implemented * Updated history and help files * Help text for --show-keys command is now specific to symmetric-key enrollments and groups * Added warning when --show-keys is used for non-symmetric-key enrollments or groups * Attempt to link hub in DPS integration tests if not already linked. --- HISTORY.rst | 14 + azext_iot/_factory.py | 2 +- azext_iot/_help.py | 14 + azext_iot/_params.py | 16 + azext_iot/operations/dps.py | 237 ++-- azext_iot/sdk/dps/__init__.py | 17 +- azext_iot/sdk/dps/models/__init__.py | 58 - azext_iot/sdk/dps/operations/__init__.py | 16 - .../device_enrollment_group_operations.py | 347 ------ .../device_enrollment_operations.py | 409 ------- .../registration_state_operations.py | 202 ---- .../sdk/dps/provisioning_service_client.py | 80 -- azext_iot/sdk/dps/service/__init__.py | 18 + azext_iot/sdk/dps/service/models/__init__.py | 94 ++ .../service/models/attestation_mechanism.py | 47 + .../models/attestation_mechanism_py3.py} | 26 +- .../models/bulk_enrollment_group_operation.py | 40 + .../bulk_enrollment_group_operation_error.py | 44 + ...lk_enrollment_group_operation_error_py3.py | 44 + .../bulk_enrollment_group_operation_py3.py | 40 + .../bulk_enrollment_group_operation_result.py | 39 + ...k_enrollment_group_operation_result_py3.py | 39 + .../models/bulk_enrollment_operation.py | 40 + .../models/bulk_enrollment_operation_error.py | 44 + .../bulk_enrollment_operation_error_py3.py} | 16 +- .../models/bulk_enrollment_operation_py3.py} | 26 +- .../bulk_enrollment_operation_result.py | 39 + .../bulk_enrollment_operation_result_py3.py} | 25 +- .../models/custom_allocation_definition.py | 42 + .../custom_allocation_definition_py3.py} | 19 +- .../dps/service/models/device_capabilities.py | 35 + .../models/device_capabilities_py3.py} | 14 +- .../models/device_registration_state.py | 20 +- .../models/device_registration_state_py3.py} | 49 +- .../dps/service/models/enrollment_group.py | 106 ++ .../models/enrollment_group_py3.py} | 41 +- .../service/models/individual_enrollment.py | 116 ++ .../models/individual_enrollment_py3.py} | 48 +- .../sdk/dps/service/models/initial_twin.py | 32 + .../service/models/initial_twin_properties.py | 28 + .../models/initial_twin_properties_py3.py} | 13 +- .../models/initial_twin_py3.py} | 14 +- azext_iot/sdk/dps/service/models/metadata.py | 33 + .../models/metadata_py3.py} | 12 +- .../provisioning_service_error_details.py | 58 + ...provisioning_service_error_details_py3.py} | 11 +- .../dps/service/models/query_specification.py | 34 + .../models/query_specification_py3.py} | 12 +- .../dps/service/models/reprovision_policy.py | 48 + .../models/reprovision_policy_py3.py} | 22 +- .../models/symmetric_key_attestation.py | 32 + .../models/symmetric_key_attestation_py3.py} | 8 +- .../sdk/dps/service/models/tpm_attestation.py | 38 + .../models/tpm_attestation_py3.py} | 12 +- .../sdk/dps/service/models/twin_collection.py | 41 + .../models/twin_collection_py3.py} | 19 +- .../dps/service/models/x509_attestation.py | 36 + .../models/x509_attestation_py3.py} | 17 +- .../dps/service/models/x509_ca_references.py | 32 + .../models/x509_ca_references_py3.py} | 8 +- .../service/models/x509_certificate_info.py | 69 ++ .../models/x509_certificate_info_py3.py} | 26 +- .../models/x509_certificate_with_info.py | 32 + .../models/x509_certificate_with_info_py3.py} | 11 +- .../dps/service/models/x509_certificates.py | 32 + .../models/x509_certificates_py3.py} | 14 +- .../service/provisioning_service_client.py | 1023 +++++++++++++++++ azext_iot/sdk/dps/{ => service}/version.py | 6 +- azext_iot/tests/test_iot_dps_int.py | 820 ++++++++----- azext_iot/tests/test_iot_dps_unit.py | 470 +++++--- 70 files changed, 3741 insertions(+), 1845 deletions(-) delete mode 100644 azext_iot/sdk/dps/models/__init__.py delete mode 100644 azext_iot/sdk/dps/operations/__init__.py delete mode 100644 azext_iot/sdk/dps/operations/device_enrollment_group_operations.py delete mode 100644 azext_iot/sdk/dps/operations/device_enrollment_operations.py delete mode 100644 azext_iot/sdk/dps/operations/registration_state_operations.py delete mode 100644 azext_iot/sdk/dps/provisioning_service_client.py create mode 100644 azext_iot/sdk/dps/service/__init__.py create mode 100644 azext_iot/sdk/dps/service/models/__init__.py create mode 100644 azext_iot/sdk/dps/service/models/attestation_mechanism.py rename azext_iot/sdk/dps/{models/attestation_mechanism.py => service/models/attestation_mechanism_py3.py} (61%) create mode 100644 azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation.py create mode 100644 azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_error.py create mode 100644 azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_error_py3.py create mode 100644 azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_py3.py create mode 100644 azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_result.py create mode 100644 azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_result_py3.py create mode 100644 azext_iot/sdk/dps/service/models/bulk_enrollment_operation.py create mode 100644 azext_iot/sdk/dps/service/models/bulk_enrollment_operation_error.py rename azext_iot/sdk/dps/{models/bulk_enrollment_operation_error.py => service/models/bulk_enrollment_operation_error_py3.py} (65%) rename azext_iot/sdk/dps/{models/bulk_enrollment_operation.py => service/models/bulk_enrollment_operation_py3.py} (52%) create mode 100644 azext_iot/sdk/dps/service/models/bulk_enrollment_operation_result.py rename azext_iot/sdk/dps/{models/bulk_enrollment_operation_result.py => service/models/bulk_enrollment_operation_result_py3.py} (57%) create mode 100644 azext_iot/sdk/dps/service/models/custom_allocation_definition.py rename azext_iot/sdk/dps/{models/custom_allocation_definition.py => service/models/custom_allocation_definition_py3.py} (55%) create mode 100644 azext_iot/sdk/dps/service/models/device_capabilities.py rename azext_iot/sdk/dps/{models/device_capabilities.py => service/models/device_capabilities_py3.py} (57%) rename azext_iot/sdk/dps/{ => service}/models/device_registration_state.py (85%) rename azext_iot/sdk/dps/{models/individual_enrollment_registration_state.py => service/models/device_registration_state_py3.py} (58%) create mode 100644 azext_iot/sdk/dps/service/models/enrollment_group.py rename azext_iot/sdk/dps/{models/enrollment_group.py => service/models/enrollment_group_py3.py} (76%) create mode 100644 azext_iot/sdk/dps/service/models/individual_enrollment.py rename azext_iot/sdk/dps/{models/individual_enrollment.py => service/models/individual_enrollment_py3.py} (76%) create mode 100644 azext_iot/sdk/dps/service/models/initial_twin.py create mode 100644 azext_iot/sdk/dps/service/models/initial_twin_properties.py rename azext_iot/sdk/dps/{models/initial_twin_properties.py => service/models/initial_twin_properties_py3.py} (59%) rename azext_iot/sdk/dps/{models/initial_twin.py => service/models/initial_twin_py3.py} (66%) create mode 100644 azext_iot/sdk/dps/service/models/metadata.py rename azext_iot/sdk/dps/{models/metadata.py => service/models/metadata_py3.py} (68%) create mode 100644 azext_iot/sdk/dps/service/models/provisioning_service_error_details.py rename azext_iot/sdk/dps/{models/provisioning_service_error_details.py => service/models/provisioning_service_error_details_py3.py} (77%) create mode 100644 azext_iot/sdk/dps/service/models/query_specification.py rename azext_iot/sdk/dps/{models/query_specification.py => service/models/query_specification_py3.py} (61%) create mode 100644 azext_iot/sdk/dps/service/models/reprovision_policy.py rename azext_iot/sdk/dps/{models/reprovision_policy.py => service/models/reprovision_policy_py3.py} (60%) create mode 100644 azext_iot/sdk/dps/service/models/symmetric_key_attestation.py rename azext_iot/sdk/dps/{models/symmetric_key_attestation.py => service/models/symmetric_key_attestation_py3.py} (72%) create mode 100644 azext_iot/sdk/dps/service/models/tpm_attestation.py rename azext_iot/sdk/dps/{models/tpm_attestation.py => service/models/tpm_attestation_py3.py} (66%) create mode 100644 azext_iot/sdk/dps/service/models/twin_collection.py rename azext_iot/sdk/dps/{models/twin_collection.py => service/models/twin_collection_py3.py} (62%) create mode 100644 azext_iot/sdk/dps/service/models/x509_attestation.py rename azext_iot/sdk/dps/{models/x509_attestation.py => service/models/x509_attestation_py3.py} (65%) create mode 100644 azext_iot/sdk/dps/service/models/x509_ca_references.py rename azext_iot/sdk/dps/{models/x509_ca_references.py => service/models/x509_ca_references_py3.py} (71%) create mode 100644 azext_iot/sdk/dps/service/models/x509_certificate_info.py rename azext_iot/sdk/dps/{models/x509_certificate_info.py => service/models/x509_certificate_info_py3.py} (71%) create mode 100644 azext_iot/sdk/dps/service/models/x509_certificate_with_info.py rename azext_iot/sdk/dps/{models/x509_certificate_with_info.py => service/models/x509_certificate_with_info_py3.py} (67%) create mode 100644 azext_iot/sdk/dps/service/models/x509_certificates.py rename azext_iot/sdk/dps/{models/x509_certificates.py => service/models/x509_certificates_py3.py} (64%) create mode 100644 azext_iot/sdk/dps/service/provisioning_service_client.py rename azext_iot/sdk/dps/{ => service}/version.py (63%) diff --git a/HISTORY.rst b/HISTORY.rst index 0127b7937..11790f457 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,20 @@ Release History =============== +0.10.0 ++++++++++++++++ + +**Breaking Changes** + +* `az iot dps enrollment show` and `az iot dps enrollment-group show` now return raw service results instead of deserialized models. + This means that some properties that were previously returned as `null` for these commands will no longer be returned, possibly causing a breaking change. + +**IoT DPS updates** + +* Added --show-keys argument to `dps enrollment show` and `dps enrollment-group show` to include full attestation information for symmetric key enrollments and enrollment groups +* Regenerated 2019-03-31 DPS Service SDK + + 0.9.9 +++++++++++++++ diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py index 286eb0b63..a64cc0901 100644 --- a/azext_iot/_factory.py +++ b/azext_iot/_factory.py @@ -118,7 +118,7 @@ def _get_pnp_runtime_sdk(self): return IotHubGatewayServiceAPIs(credentials=credentials, base_url=self.endpoint) def _get_dps_service_sdk(self): - from azext_iot.sdk.dps import ProvisioningServiceClient + from azext_iot.sdk.dps.service import ProvisioningServiceClient credentials = SasTokenAuthentication( uri=self.sas_uri, diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 7c0ce4185..6fbae8bed 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -1007,6 +1007,13 @@ ] = """ type: command short-summary: Get device enrollment details in an Azure IoT Hub Device Provisioning Service. + examples: + - name: Basic usage + text: > + az iot dps enrollment show --dps-name {dps_name} -g {resource_group} --enrollment-id {enrollment_id} + - name: Include full attestation information in results for a symmetric key enrollment + text: > + az iot dps enrollment show --dps-name {dps_name} -g {resource_group} --enrollment-id {symmetric_key_enrollment_id} --show-keys """ helps[ @@ -1133,6 +1140,13 @@ ] = """ type: command short-summary: Get the details of an enrollment group in an Azure IoT Hub Device Provisioning Service. + examples: + - name: Basic usage + text: > + az iot dps enrollment-group show --dps-name {dps_name} -g {resource_group} --enrollment-id {enrollment_id} + - name: Include full attestation information in results for a symmetric key enrollment-group + text: > + az iot dps enrollment-group show --dps-name {dps_name} -g {resource_group} --enrollment-id {symmetric_key_enrollment_id} --show-keys """ helps[ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 698bb8cec..e21213e39 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -861,6 +861,14 @@ def load_arguments(self, _): "When choosing tpm as attestation type, endorsement key is required.", ) + with self.argument_context("iot dps enrollment show") as context: + context.argument( + "show_keys", + options_list=["--show-keys", "--keys"], + arg_type=get_three_state_flag(), + help="Include attestation keys and information in enrollment results" + ) + with self.argument_context("iot dps enrollment update") as context: context.argument( "endorsement_key", @@ -905,6 +913,14 @@ def load_arguments(self, _): "If attestation with a root CA certificate is desired then a root ca name must be provided.", ) + with self.argument_context("iot dps enrollment-group show") as context: + context.argument( + "show_keys", + options_list=["--show-keys", "--keys"], + arg_type=get_three_state_flag(), + help="Include attestation keys and information in enrollment group results" + ) + with self.argument_context("iot dps registration") as context: context.argument("registration_id", help="ID of device registration") diff --git a/azext_iot/operations/dps.py b/azext_iot/operations/dps.py index b46ef951d..a9962c9a6 100644 --- a/azext_iot/operations/dps.py +++ b/azext_iot/operations/dps.py @@ -18,27 +18,24 @@ from azext_iot.common.utility import compute_device_key from azext_iot.operations.generic import _execute_query from azext_iot._factory import SdkResolver -from azext_iot.sdk.dps.models.individual_enrollment import IndividualEnrollment -from azext_iot.sdk.dps.models.custom_allocation_definition import ( +from azext_iot.sdk.dps.service.models import ( + IndividualEnrollment, CustomAllocationDefinition, -) -from azext_iot.sdk.dps.models.attestation_mechanism import AttestationMechanism -from azext_iot.sdk.dps.models.tpm_attestation import TpmAttestation -from azext_iot.sdk.dps.models.symmetric_key_attestation import SymmetricKeyAttestation -from azext_iot.sdk.dps.models.x509_attestation import X509Attestation -from azext_iot.sdk.dps.models.x509_certificates import X509Certificates -from azext_iot.sdk.dps.models.x509_certificate_with_info import X509CertificateWithInfo -from azext_iot.sdk.dps.models.initial_twin import InitialTwin -from azext_iot.sdk.dps.models.twin_collection import TwinCollection -from azext_iot.sdk.dps.models.initial_twin_properties import InitialTwinProperties -from azext_iot.sdk.dps.models.enrollment_group import EnrollmentGroup -from azext_iot.sdk.dps.models.x509_ca_references import X509CAReferences -from azext_iot.sdk.dps.models.reprovision_policy import ReprovisionPolicy -from azext_iot.sdk.dps.models import DeviceCapabilities -from azext_iot.sdk.dps.models import ( + AttestationMechanism, + TpmAttestation, + SymmetricKeyAttestation, + X509Attestation, + X509Certificates, + X509CertificateWithInfo, + InitialTwin, + TwinCollection, + InitialTwinProperties, + EnrollmentGroup, + X509CAReferences, + ReprovisionPolicy, + DeviceCapabilities, ProvisioningServiceErrorDetailsException, -) # TODO: Regen SDK - +) logger = get_logger(__name__) @@ -47,7 +44,7 @@ def iot_dps_device_enrollment_list(client, dps_name, resource_group_name, top=None): - from azext_iot.sdk.dps.models.query_specification import QuerySpecification + from azext_iot.sdk.dps.service.models.query_specification import QuerySpecification target = get_iot_dps_connection_string(client, dps_name, resource_group_name) @@ -56,19 +53,38 @@ def iot_dps_device_enrollment_list(client, dps_name, resource_group_name, top=No sdk = resolver.get_sdk(SdkType.dps_sdk) query_command = "SELECT *" - query = [QuerySpecification(query_command)] - return _execute_query(query, sdk.device_enrollment.query, top) + query = [QuerySpecification(query=query_command)] + return _execute_query(query, sdk.query_individual_enrollments, top) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) -def iot_dps_device_enrollment_get(client, enrollment_id, dps_name, resource_group_name): +def iot_dps_device_enrollment_get( + client, enrollment_id, dps_name, resource_group_name, show_keys=None +): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.device_enrollment.get(enrollment_id) + enrollment = sdk.get_individual_enrollment( + enrollment_id, raw=True + ).response.json() + if show_keys: + enrollment_type = enrollment["attestation"]["type"] + if enrollment_type == AttestationType.symmetricKey.value: + attestation = sdk.get_individual_enrollment_attestation_mechanism( + enrollment_id, raw=True + ).response.json() + enrollment["attestation"] = attestation + else: + logger.warn( + "--show-keys argument was provided, but requested enrollment has an attestation type of '{}'." + " Currently, --show-keys is only supported for symmetric key enrollments".format( + enrollment_type + ) + ) + return enrollment except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -105,7 +121,8 @@ def iot_dps_device_enrollment_create( if not endorsement_key: raise CLIError("Endorsement key is requried") attestation = AttestationMechanism( - AttestationType.tpm.value, TpmAttestation(endorsement_key) + type=AttestationType.tpm.value, + tpm=TpmAttestation(endorsement_key=endorsement_key), ) if attestation_type == AttestationType.x509.value: attestation = _get_attestation_with_x509_client_cert( @@ -113,10 +130,10 @@ def iot_dps_device_enrollment_create( ) if attestation_type == AttestationType.symmetricKey.value: attestation = AttestationMechanism( - AttestationType.symmetricKey.value, - None, - None, - SymmetricKeyAttestation(primary_key, secondary_key), + type=AttestationType.symmetricKey.value, + symmetric_key=SymmetricKeyAttestation( + primary_key=primary_key, secondary_key=secondary_key + ), ) reprovision = _get_reprovision_policy(reprovision_policy) initial_twin = _get_initial_twin(initial_twin_tags, initial_twin_properties) @@ -129,26 +146,24 @@ def iot_dps_device_enrollment_create( iot_hub_list = iot_hub_host_name.split() custom_allocation_definition = ( - CustomAllocationDefinition(webhook_url, api_version) + CustomAllocationDefinition(webhook_url=webhook_url, api_version=api_version) if allocation_policy == AllocationType.custom.value else None ) capabilities = DeviceCapabilities(iot_edge=edge_enabled) enrollment = IndividualEnrollment( - enrollment_id, - attestation, - capabilities, - device_id, - None, - initial_twin, - None, - provisioning_status, - reprovision, - allocation_policy, - iot_hub_list, - custom_allocation_definition, + registration_id=enrollment_id, + attestation=attestation, + capabilities=capabilities, + device_id=device_id, + initial_twin=initial_twin, + provisioning_status=provisioning_status, + reprovision_policy=reprovision, + allocation_policy=allocation_policy, + iot_hubs=iot_hub_list, + custom_allocation_definition=custom_allocation_definition, ) - return sdk.device_enrollment.create_or_update(enrollment_id, enrollment) + return sdk.create_or_update_individual_enrollment(enrollment_id, enrollment) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -183,7 +198,7 @@ def iot_dps_device_enrollment_update( resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - enrollment_record = sdk.device_enrollment.get(enrollment_id) + enrollment_record = sdk.get_individual_enrollment(enrollment_id) # Verify etag if ( etag @@ -217,7 +232,7 @@ def iot_dps_device_enrollment_update( remove_secondary_certificate, ) else: - enrollment_record.attestation = sdk.device_enrollment.attestation_mechanism_method( + enrollment_record.attestation = sdk.get_individual_enrollment_attestation_mechanism( enrollment_id ) if primary_key: @@ -253,12 +268,12 @@ def iot_dps_device_enrollment_update( enrollment_record.iot_hub_host_name = None if allocation_policy == AllocationType.custom.value: enrollment_record.custom_allocation_definition = CustomAllocationDefinition( - webhook_url, api_version + webhook_url=webhook_url, api_version=api_version ) if edge_enabled is not None: enrollment_record.capabilities = DeviceCapabilities(iot_edge=edge_enabled) - return sdk.device_enrollment.create_or_update( + return sdk.create_or_update_individual_enrollment( enrollment_id, enrollment_record, etag ) except ProvisioningServiceErrorDetailsException as e: @@ -273,7 +288,7 @@ def iot_dps_device_enrollment_delete( resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.device_enrollment.delete(enrollment_id) + return sdk.delete_individual_enrollment(enrollment_id) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -284,7 +299,7 @@ def iot_dps_device_enrollment_delete( def iot_dps_device_enrollment_group_list( client, dps_name, resource_group_name, top=None ): - from azext_iot.sdk.dps.models.query_specification import QuerySpecification + from azext_iot.sdk.dps.service.models.query_specification import QuerySpecification target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: @@ -292,21 +307,38 @@ def iot_dps_device_enrollment_group_list( sdk = resolver.get_sdk(SdkType.dps_sdk) query_command = "SELECT *" - query1 = [QuerySpecification(query_command)] - return _execute_query(query1, sdk.device_enrollment_group.query, top) + query1 = [QuerySpecification(query=query_command)] + return _execute_query(query1, sdk.query_enrollment_groups, top) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) def iot_dps_device_enrollment_group_get( - client, enrollment_id, dps_name, resource_group_name + client, enrollment_id, dps_name, resource_group_name, show_keys=None ): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.device_enrollment_group.get(enrollment_id) + enrollment_group = sdk.get_enrollment_group( + enrollment_id, raw=True + ).response.json() + if show_keys: + enrollment_type = enrollment_group["attestation"]["type"] + if enrollment_type == AttestationType.symmetricKey.value: + attestation = sdk.get_enrollment_group_attestation_mechanism( + enrollment_id, raw=True + ).response.json() + enrollment_group["attestation"] = attestation + else: + logger.warn( + "--show-keys argument was provided, but requested enrollment group has an attestation type of '{}'." + " Currently, --show-keys is only supported for symmetric key enrollment groups".format( + enrollment_type + ) + ) + return enrollment_group except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -341,10 +373,10 @@ def iot_dps_device_enrollment_group_create( if not certificate_path and not secondary_certificate_path: if not root_ca_name and not secondary_root_ca_name: attestation = AttestationMechanism( - AttestationType.symmetricKey.value, - None, - None, - SymmetricKeyAttestation(primary_key, secondary_key), + type=AttestationType.symmetricKey.value, + symmetric_key=SymmetricKeyAttestation( + primary_key=primary_key, secondary_key=secondary_key + ), ) if certificate_path or secondary_certificate_path: if root_ca_name or secondary_root_ca_name: @@ -373,28 +405,24 @@ def iot_dps_device_enrollment_group_create( iot_hub_list = iot_hub_host_name.split() custom_allocation_definition = ( - CustomAllocationDefinition(webhook_url, api_version) + CustomAllocationDefinition(webhook_url=webhook_url, api_version=api_version) if allocation_policy == AllocationType.custom.value else None ) capabilities = DeviceCapabilities(iot_edge=edge_enabled) group_enrollment = EnrollmentGroup( - enrollment_id, - attestation, - capabilities, - None, - initial_twin, - None, - provisioning_status, - reprovision, - allocation_policy, - iot_hub_list, - custom_allocation_definition, - ) - return sdk.device_enrollment_group.create_or_update( - enrollment_id, group_enrollment + enrollment_group_id=enrollment_id, + attestation=attestation, + capabilities=capabilities, + initial_twin=initial_twin, + provisioning_status=provisioning_status, + reprovision_policy=reprovision, + allocation_policy=allocation_policy, + iot_hubs=iot_hub_list, + custom_allocation_definition=custom_allocation_definition, ) + return sdk.create_or_update_enrollment_group(enrollment_id, group_enrollment) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -429,7 +457,7 @@ def iot_dps_device_enrollment_group_update( resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - enrollment_record = sdk.device_enrollment_group.get(enrollment_id) + enrollment_record = sdk.get_enrollment_group(enrollment_id) # Verify etag if ( etag @@ -441,7 +469,7 @@ def iot_dps_device_enrollment_group_update( etag = enrollment_record.etag.replace('"', "") # Update enrollment information if enrollment_record.attestation.type == AttestationType.symmetricKey.value: - enrollment_record.attestation = sdk.device_enrollment_group.attestation_mechanism_method( + enrollment_record.attestation = sdk.get_enrollment_group_attestation_mechanism( enrollment_id ) if primary_key: @@ -519,11 +547,11 @@ def iot_dps_device_enrollment_group_update( enrollment_record.iot_hub_host_name = None if allocation_policy == AllocationType.custom.value: enrollment_record.custom_allocation_definition = CustomAllocationDefinition( - webhook_url, api_version + webhook_url=webhook_url, api_version=api_version ) if edge_enabled is not None: enrollment_record.capabilities = DeviceCapabilities(iot_edge=edge_enabled) - return sdk.device_enrollment_group.create_or_update( + return sdk.create_or_update_enrollment_group( enrollment_id, enrollment_record, etag ) except ProvisioningServiceErrorDetailsException as e: @@ -538,7 +566,7 @@ def iot_dps_device_enrollment_group_delete( resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.device_enrollment_group.delete(enrollment_id) + return sdk.delete_enrollment_group(enrollment_id) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -560,7 +588,9 @@ def iot_dps_registration_list(client, dps_name, resource_group_name, enrollment_ resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.registration_state.query_registration_state(enrollment_id) + return sdk.query_device_registration_states( + enrollment_id, raw=True + ).response.json() except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -571,7 +601,9 @@ def iot_dps_registration_get(client, dps_name, resource_group_name, registration resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.registration_state.get_registration_state(registration_id) + return sdk.get_device_registration_state( + registration_id, raw=True + ).response.json() except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -582,7 +614,7 @@ def iot_dps_registration_delete(client, dps_name, resource_group_name, registrat resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.registration_state.delete_registration_state(registration_id) + return sdk.delete_device_registration_state(registration_id) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -601,9 +633,12 @@ def _get_initial_twin(initial_twin_tags=None, initial_twin_properties=None): initial_twin_properties = dict_clean( shell_safe_json_parse(str(initial_twin_properties)) ) + return InitialTwin( - TwinCollection(initial_twin_tags), - InitialTwinProperties(TwinCollection(initial_twin_properties)), + tags=TwinCollection(additional_properties=initial_twin_tags), + properties=InitialTwinProperties( + desired=TwinCollection(additional_properties=initial_twin_properties) + ), ) @@ -626,8 +661,8 @@ def _get_updated_inital_twin( def _get_x509_certificate(certificate_path, secondary_certificate_path): x509certificate = X509Certificates( - _get_certificate_info(certificate_path), - _get_certificate_info(secondary_certificate_path), + primary=_get_certificate_info(certificate_path), + secondary=_get_certificate_info(secondary_certificate_path), ) return x509certificate @@ -636,7 +671,7 @@ def _get_certificate_info(certificate_path): if not certificate_path: return None certificate_content = open_certificate(certificate_path) - certificate_with_info = X509CertificateWithInfo(certificate_content) + certificate_with_info = X509CertificateWithInfo(certificate=certificate_content) return certificate_with_info @@ -648,9 +683,9 @@ def _get_attestation_with_x509_client_cert( certificate = _get_x509_certificate( primary_certificate_path, secondary_certificate_path ) - x509Attestation = X509Attestation(certificate) + x509Attestation = X509Attestation(client_certificates=certificate) attestation = AttestationMechanism( - AttestationType.x509.value, None, x509Attestation + type=AttestationType.x509.value, x509=x509Attestation ) return attestation @@ -683,18 +718,20 @@ def _get_attestation_with_x509_signing_cert( certificate = _get_x509_certificate( primary_certificate_path, secondary_certificate_path ) - x509Attestation = X509Attestation(None, certificate) + x509Attestation = X509Attestation(signing_certificates=certificate) attestation = AttestationMechanism( - AttestationType.x509.value, None, x509Attestation + type=AttestationType.x509.value, x509=x509Attestation ) return attestation def _get_attestation_with_x509_ca_cert(root_ca_name, secondary_root_ca_name): - certificate = X509CAReferences(root_ca_name, secondary_root_ca_name) - x509Attestation = X509Attestation(None, None, certificate) + certificate = X509CAReferences( + primary=root_ca_name, secondary=secondary_root_ca_name + ) + x509Attestation = X509Attestation(ca_references=certificate) attestation = AttestationMechanism( - AttestationType.x509.value, None, x509Attestation + type=AttestationType.x509.value, x509=x509Attestation ) return attestation @@ -785,15 +822,23 @@ def _can_remove_secondary_certificate(remove_certificate, attestation): def _get_reprovision_policy(reprovision_policy): if reprovision_policy: if reprovision_policy == ReprovisionType.reprovisionandmigratedata.value: - reprovision = ReprovisionPolicy(True, True) + reprovision = ReprovisionPolicy( + update_hub_assignment=True, migrate_device_data=True + ) elif reprovision_policy == ReprovisionType.reprovisionandresetdata.value: - reprovision = ReprovisionPolicy(True, False) + reprovision = ReprovisionPolicy( + update_hub_assignment=True, migrate_device_data=False + ) elif reprovision_policy == ReprovisionType.never.value: - reprovision = ReprovisionPolicy(False, False) + reprovision = ReprovisionPolicy( + update_hub_assignment=False, migrate_device_data=False + ) else: raise CLIError("Invalid Reprovision Policy.") else: - reprovision = ReprovisionPolicy(True, True) + reprovision = ReprovisionPolicy( + update_hub_assignment=True, migrate_device_data=True + ) return reprovision diff --git a/azext_iot/sdk/dps/__init__.py b/azext_iot/sdk/dps/__init__.py index 7cc65f931..55614acbf 100644 --- a/azext_iot/sdk/dps/__init__.py +++ b/azext_iot/sdk/dps/__init__.py @@ -1,14 +1,5 @@ # coding=utf-8 -# -------------------------------------------------------------------------- -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .provisioning_service_client import ProvisioningServiceClient -from .version import VERSION - -__all__ = ['ProvisioningServiceClient'] - -__version__ = VERSION - +# -------------------------------------------------------------------------------------------- +# 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/sdk/dps/models/__init__.py b/azext_iot/sdk/dps/models/__init__.py deleted file mode 100644 index 0c8326d2e..000000000 --- a/azext_iot/sdk/dps/models/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .provisioning_service_error_details import ProvisioningServiceErrorDetails, ProvisioningServiceErrorDetailsException -from .device_capabilities import DeviceCapabilities -from .individual_enrollment_registration_state import IndividualEnrollmentRegistrationState -from .tpm_attestation import TpmAttestation -from .x509_certificate_info import X509CertificateInfo -from .x509_certificate_with_info import X509CertificateWithInfo -from .x509_certificates import X509Certificates -from .x509_ca_references import X509CAReferences -from .x509_attestation import X509Attestation -from .symmetric_key_attestation import SymmetricKeyAttestation -from .attestation_mechanism import AttestationMechanism -from .metadata import Metadata -from .twin_collection import TwinCollection -from .initial_twin_properties import InitialTwinProperties -from .initial_twin import InitialTwin -from .reprovision_policy import ReprovisionPolicy -from .custom_allocation_definition import CustomAllocationDefinition -from .individual_enrollment import IndividualEnrollment -from .device_registration_state import DeviceRegistrationState -from .enrollment_group import EnrollmentGroup -from .bulk_enrollment_operation import BulkEnrollmentOperation -from .bulk_enrollment_operation_error import BulkEnrollmentOperationError -from .bulk_enrollment_operation_result import BulkEnrollmentOperationResult -from .query_specification import QuerySpecification - -__all__ = [ - 'ProvisioningServiceErrorDetails', 'ProvisioningServiceErrorDetailsException', - 'DeviceCapabilities', - 'IndividualEnrollmentRegistrationState', - 'TpmAttestation', - 'X509CertificateInfo', - 'X509CertificateWithInfo', - 'X509Certificates', - 'X509CAReferences', - 'X509Attestation', - 'SymmetricKeyAttestation', - 'AttestationMechanism', - 'Metadata', - 'TwinCollection', - 'InitialTwinProperties', - 'InitialTwin', - 'ReprovisionPolicy', - 'CustomAllocationDefinition', - 'IndividualEnrollment', - 'DeviceRegistrationState', - 'EnrollmentGroup', - 'BulkEnrollmentOperation', - 'BulkEnrollmentOperationError', - 'BulkEnrollmentOperationResult', - 'QuerySpecification', -] diff --git a/azext_iot/sdk/dps/operations/__init__.py b/azext_iot/sdk/dps/operations/__init__.py deleted file mode 100644 index 54916bd89..000000000 --- a/azext_iot/sdk/dps/operations/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .device_enrollment_operations import DeviceEnrollmentOperations -from .device_enrollment_group_operations import DeviceEnrollmentGroupOperations -from .registration_state_operations import RegistrationStateOperations - -__all__ = [ - 'DeviceEnrollmentOperations', - 'DeviceEnrollmentGroupOperations', - 'RegistrationStateOperations', -] diff --git a/azext_iot/sdk/dps/operations/device_enrollment_group_operations.py b/azext_iot/sdk/dps/operations/device_enrollment_group_operations.py deleted file mode 100644 index 2ada50461..000000000 --- a/azext_iot/sdk/dps/operations/device_enrollment_group_operations.py +++ /dev/null @@ -1,347 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- -import uuid -from msrest.pipeline import ClientRawResponse - -from .. import models - - -class DeviceEnrollmentGroupOperations(object): - """DeviceEnrollmentGroupOperations operations. - - :param client: Client for service requests. - :param config: Configuration of service client. - :param serializer: An object model serializer. - :param deserializer: An object model deserializer. - :ivar api_version: The API version to use for the request. Supported versions include: 2018-09-01-preview. Constant value: "2018-09-01-preview". - """ - - models = models - - def __init__(self, client, config, serializer, deserializer): - - self._client = client - self._serialize = serializer - self._deserialize = deserializer - self.api_version = "2019-03-31" - - self.config = config - - def get( - self, id, custom_headers=None, raw=False, **operation_config): - """Get a device enrollment group. - - :param id: Enrollment group ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: EnrollmentGroup or ClientRawResponse if raw=true - :rtype: - ~microsoft.azure.management.provisioningservices.models.EnrollmentGroup - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.get.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('EnrollmentGroup', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get.metadata = {'url': '/enrollmentGroups/{id}'} - - def create_or_update( - self, id, enrollment_group, if_match=None, custom_headers=None, raw=False, **operation_config): - """Create or update a device enrollment group. - - :param id: Enrollment group ID. - :type id: str - :param enrollment_group: The device enrollment group. - :type enrollment_group: - ~microsoft.azure.management.provisioningservices.models.EnrollmentGroup - :param if_match: The ETag of the enrollment record. - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: EnrollmentGroup or ClientRawResponse if raw=true - :rtype: - ~microsoft.azure.management.provisioningservices.models.EnrollmentGroup - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.create_or_update.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(enrollment_group, 'EnrollmentGroup') - - # Construct and send request - request = self._client.put(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('EnrollmentGroup', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_or_update.metadata = {'url': '/enrollmentGroups/{id}'} - - def delete( - self, id, if_match=None, custom_headers=None, raw=False, **operation_config): - """Delete a device enrollment group. - - :param id: Enrollment group ID. - :type id: str - :param if_match: The ETag of the enrollment group record. - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.delete.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [204]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - delete.metadata = {'url': '/enrollmentGroups/{id}'} - - def query( - self, query_specification, x_ms_max_item_count=None, x_ms_continuation=None, custom_headers=None, raw=False, **operation_config): - """Query the device enrollment groups. - - :param query_specification: The query specification. - :type query_specification: - ~microsoft.azure.management.provisioningservices.models.QuerySpecification - :param x_ms_max_item_count: pageSize - :type x_ms_max_item_count: int - :param x_ms_continuation: continuation token - :type x_ms_continuation: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: - list[~microsoft.azure.management.provisioningservices.models.EnrollmentGroup] - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.query.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_max_item_count is not None: - header_parameters['x-ms-max-item-count'] = self._serialize.header("x_ms_max_item_count", x_ms_max_item_count, 'int') - if x_ms_continuation is not None: - header_parameters['x-ms-continuation'] = self._serialize.header("x_ms_continuation", x_ms_continuation, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(query_specification, 'QuerySpecification') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('[EnrollmentGroup]', response) - header_dict = { - 'x-ms-continuation': 'str', - 'x-ms-max-item-count': 'int', - 'x-ms-item-type': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - # Added Custom - continuation = response.headers.get('x-ms-continuation') - - return deserialized, continuation - query.metadata = {'url': '/enrollmentGroups/query'} - - def attestation_mechanism_method( - self, id, custom_headers=None, raw=False, **operation_config): - """Get the attestation mechanism in the device enrollment group record. - - :param id: Enrollment group ID - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: AttestationMechanism or ClientRawResponse if raw=true - :rtype: - ~microsoft.azure.management.provisioningservices.models.AttestationMechanism - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.attestation_mechanism_method.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('AttestationMechanism', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - attestation_mechanism_method.metadata = {'url': '/enrollmentGroups/{id}/attestationmechanism'} diff --git a/azext_iot/sdk/dps/operations/device_enrollment_operations.py b/azext_iot/sdk/dps/operations/device_enrollment_operations.py deleted file mode 100644 index 277d7c626..000000000 --- a/azext_iot/sdk/dps/operations/device_enrollment_operations.py +++ /dev/null @@ -1,409 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -import uuid -from msrest.pipeline import ClientRawResponse - -from .. import models - - -class DeviceEnrollmentOperations(object): - """DeviceEnrollmentOperations operations. - - :param client: Client for service requests. - :param config: Configuration of service client. - :param serializer: An object model serializer. - :param deserializer: An object model deserializer. - :ivar api_version: The API version to use for the request. Supported versions include: 2018-09-01-preview. Constant value: "2018-09-01-preview". - """ - - models = models - - def __init__(self, client, config, serializer, deserializer): - - self._client = client - self._serialize = serializer - self._deserialize = deserializer - self.api_version = "2019-03-31" - - self.config = config - - def get( - self, id, custom_headers=None, raw=False, **operation_config): - """Get a device enrollment record. - - :param id: Registration ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: IndividualEnrollment or ClientRawResponse if raw=true - :rtype: - ~microsoft.azure.management.provisioningservices.models.IndividualEnrollment - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.get.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('IndividualEnrollment', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get.metadata = {'url': '/enrollments/{id}'} - - def create_or_update( - self, id, enrollment, if_match=None, custom_headers=None, raw=False, **operation_config): - """Create or update a device enrollment record. - - :param id: The registration ID is alphanumeric, lowercase, and may - contain hyphens. - :type id: str - :param enrollment: The device enrollment record. - :type enrollment: - ~microsoft.azure.management.provisioningservices.models.IndividualEnrollment - :param if_match: The ETag of the enrollment record. - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: IndividualEnrollment or ClientRawResponse if raw=true - :rtype: - ~microsoft.azure.management.provisioningservices.models.IndividualEnrollment - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.create_or_update.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(enrollment, 'IndividualEnrollment') - - # Construct and send request - request = self._client.put(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('IndividualEnrollment', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_or_update.metadata = {'url': '/enrollments/{id}'} - - def delete( - self, id, if_match=None, custom_headers=None, raw=False, **operation_config): - """Delete a device enrollment record. - - :param id: Registration ID. - :type id: str - :param if_match: The ETag of the enrollment record. - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.delete.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [204]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - delete.metadata = {'url': '/enrollments/{id}'} - - def bulk_operation( - self, bulk_operation, custom_headers=None, raw=False, **operation_config): - """Bulk device enrollment operation. - - :param bulk_operation: Bulk operation. - :type bulk_operation: - ~microsoft.azure.management.provisioningservices.models.BulkEnrollmentOperation - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: BulkEnrollmentOperationResult or ClientRawResponse if - raw=true - :rtype: - ~microsoft.azure.management.provisioningservices.models.BulkEnrollmentOperationResult - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.bulk_operation.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(bulk_operation, 'BulkEnrollmentOperation') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('BulkEnrollmentOperationResult', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - bulk_operation.metadata = {'url': '/enrollments'} - - def query( - self, query_specification, x_ms_max_item_count=None, x_ms_continuation=None, custom_headers=None, raw=False, **operation_config): - """Query the device enrollment records. - - :param query_specification: The query specification. - :type query_specification: - ~microsoft.azure.management.provisioningservices.models.QuerySpecification - :param x_ms_max_item_count: pageSize - :type x_ms_max_item_count: int - :param x_ms_continuation: continuation token - :type x_ms_continuation: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: - list[~microsoft.azure.management.provisioningservices.models.IndividualEnrollment] - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.query.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_max_item_count is not None: - header_parameters['x-ms-max-item-count'] = self._serialize.header("x_ms_max_item_count", x_ms_max_item_count, 'int') - if x_ms_continuation is not None: - header_parameters['x-ms-continuation'] = self._serialize.header("x_ms_continuation", x_ms_continuation, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(query_specification, 'QuerySpecification') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send( - request, header_parameters, body_content, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('[IndividualEnrollment]', response) - header_dict = { - 'x-ms-continuation': 'str', - 'x-ms-max-item-count': 'int', - 'x-ms-item-type': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - # Added Custom - continuation = response.headers.get('x-ms-continuation') - - return deserialized, continuation - query.metadata = {'url': '/enrollments/query'} - - def attestation_mechanism_method( - self, id, custom_headers=None, raw=False, **operation_config): - """Get the attestation mechanism in the device enrollment record. - - :param id: Registration ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: AttestationMechanism or ClientRawResponse if raw=true - :rtype: - ~microsoft.azure.management.provisioningservices.models.AttestationMechanism - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.attestation_mechanism_method.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('AttestationMechanism', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - attestation_mechanism_method.metadata = {'url': '/enrollments/{id}/attestationmechanism'} diff --git a/azext_iot/sdk/dps/operations/registration_state_operations.py b/azext_iot/sdk/dps/operations/registration_state_operations.py deleted file mode 100644 index c74ecca7e..000000000 --- a/azext_iot/sdk/dps/operations/registration_state_operations.py +++ /dev/null @@ -1,202 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -import uuid -from msrest.pipeline import ClientRawResponse - -from .. import models - - -class RegistrationStateOperations(object): - """RegistrationStateOperations operations. - - :param client: Client for service requests. - :param config: Configuration of service client. - :param serializer: An object model serializer. - :param deserializer: An object model deserializer. - :ivar api_version: The API version to use for the request. Supported versions include: 2018-09-01-preview. Constant value: "2018-09-01-preview". - """ - - models = models - - def __init__(self, client, config, serializer, deserializer): - - self._client = client - self._serialize = serializer - self._deserialize = deserializer - self.api_version = "2019-03-31" - - self.config = config - - def get_registration_state( - self, id, custom_headers=None, raw=False, **operation_config): - """Gets the device registration state. - - :param id: Registration ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: DeviceRegistrationState or ClientRawResponse if raw=true - :rtype: - ~microsoft.azure.management.provisioningservices.models.DeviceRegistrationState - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.get_registration_state.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('DeviceRegistrationState', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_registration_state.metadata = {'url': '/registrations/{id}'} - - def delete_registration_state( - self, id, if_match=None, custom_headers=None, raw=False, **operation_config): - """Deletes the device registration. - - :param id: Registration ID. - :type id: str - :param if_match: The ETag of the registration status record. - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.delete_registration_state.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [204]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - delete_registration_state.metadata = {'url': '/registrations/{id}'} - - def query_registration_state( - self, id, custom_headers=None, raw=False, **operation_config): - """Gets the registration state of devices in this enrollmentGroup. - - :param id: Enrollment group ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: - list[~microsoft.azure.management.provisioningservices.models.DeviceRegistrationState] - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ProvisioningServiceErrorDetailsException` - """ - # Construct URL - url = self.query_registration_state.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.post(url, query_parameters) - response = self._client.send(request, header_parameters, stream=False, **operation_config) - - if response.status_code not in [200]: - raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[DeviceRegistrationState]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - query_registration_state.metadata = {'url': '/registrations/{id}/query'} diff --git a/azext_iot/sdk/dps/provisioning_service_client.py b/azext_iot/sdk/dps/provisioning_service_client.py deleted file mode 100644 index ffeae8354..000000000 --- a/azext_iot/sdk/dps/provisioning_service_client.py +++ /dev/null @@ -1,80 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.service_client import ServiceClient -from msrest import Serializer, Deserializer -from msrestazure import AzureConfiguration -from .version import VERSION -from .operations.device_enrollment_operations import DeviceEnrollmentOperations -from .operations.device_enrollment_group_operations import DeviceEnrollmentGroupOperations -from .operations.registration_state_operations import RegistrationStateOperations -from . import models -from azext_iot.constants import USER_AGENT - - -class ProvisioningServiceClientConfiguration(AzureConfiguration): - """Configuration for ProvisioningServiceClient - Note that all parameters used to create this instance are saved as instance - attributes. - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - if credentials is None: - raise ValueError("Parameter 'credentials' must not be None.") - if not base_url: - base_url = 'https://localhost' - - super(ProvisioningServiceClientConfiguration, self).__init__(base_url) - self.add_user_agent('provisioningserviceclient/{}'.format(VERSION)) - self.add_user_agent(USER_AGENT) - - self.credentials = credentials - - -class ProvisioningServiceClient(object): - """API for service operations with the Azure IoT Hub Device Provisioning Service - - :ivar config: Configuration for client. - :vartype config: ProvisioningServiceClientConfiguration - - :ivar device_enrollment: DeviceEnrollment operations - :vartype device_enrollment: microsoft.azure.management.provisioningservices.operations.DeviceEnrollmentOperations - :ivar device_enrollment_group: DeviceEnrollmentGroup operations - :vartype device_enrollment_group: microsoft.azure.management.provisioningservices.operations.DeviceEnrollmentGroupOperations - :ivar registration_state: RegistrationState operations - :vartype registration_state: microsoft.azure.management.provisioningservices.operations.RegistrationStateOperations - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - self.config = ProvisioningServiceClientConfiguration(credentials, base_url) - self._client = ServiceClient(self.config.credentials, self.config) - - client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2019-03-31' # @digimaun - self._serialize = Serializer(client_models) - self._deserialize = Deserializer(client_models) - - self.device_enrollment = DeviceEnrollmentOperations( - self._client, self.config, self._serialize, self._deserialize) - self.device_enrollment_group = DeviceEnrollmentGroupOperations( - self._client, self.config, self._serialize, self._deserialize) - self.registration_state = RegistrationStateOperations( - self._client, self.config, self._serialize, self._deserialize) diff --git a/azext_iot/sdk/dps/service/__init__.py b/azext_iot/sdk/dps/service/__init__.py new file mode 100644 index 000000000..a2cf0b44c --- /dev/null +++ b/azext_iot/sdk/dps/service/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .provisioning_service_client import ProvisioningServiceClient +from .version import VERSION + +__all__ = ['ProvisioningServiceClient'] + +__version__ = VERSION + diff --git a/azext_iot/sdk/dps/service/models/__init__.py b/azext_iot/sdk/dps/service/models/__init__.py new file mode 100644 index 000000000..96dff47da --- /dev/null +++ b/azext_iot/sdk/dps/service/models/__init__.py @@ -0,0 +1,94 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +try: + from .device_registration_state_py3 import DeviceRegistrationState + from .tpm_attestation_py3 import TpmAttestation + from .x509_certificate_info_py3 import X509CertificateInfo + from .x509_certificate_with_info_py3 import X509CertificateWithInfo + from .x509_certificates_py3 import X509Certificates + from .x509_ca_references_py3 import X509CAReferences + from .x509_attestation_py3 import X509Attestation + from .symmetric_key_attestation_py3 import SymmetricKeyAttestation + from .attestation_mechanism_py3 import AttestationMechanism + from .device_capabilities_py3 import DeviceCapabilities + from .metadata_py3 import Metadata + from .twin_collection_py3 import TwinCollection + from .initial_twin_properties_py3 import InitialTwinProperties + from .initial_twin_py3 import InitialTwin + from .reprovision_policy_py3 import ReprovisionPolicy + from .custom_allocation_definition_py3 import CustomAllocationDefinition + from .individual_enrollment_py3 import IndividualEnrollment + from .enrollment_group_py3 import EnrollmentGroup + from .provisioning_service_error_details_py3 import ProvisioningServiceErrorDetails, ProvisioningServiceErrorDetailsException + from .query_specification_py3 import QuerySpecification + from .bulk_enrollment_operation_py3 import BulkEnrollmentOperation + from .bulk_enrollment_operation_error_py3 import BulkEnrollmentOperationError + from .bulk_enrollment_operation_result_py3 import BulkEnrollmentOperationResult + from .bulk_enrollment_group_operation_py3 import BulkEnrollmentGroupOperation + from .bulk_enrollment_group_operation_error_py3 import BulkEnrollmentGroupOperationError + from .bulk_enrollment_group_operation_result_py3 import BulkEnrollmentGroupOperationResult +except (SyntaxError, ImportError): + from .device_registration_state import DeviceRegistrationState + from .tpm_attestation import TpmAttestation + from .x509_certificate_info import X509CertificateInfo + from .x509_certificate_with_info import X509CertificateWithInfo + from .x509_certificates import X509Certificates + from .x509_ca_references import X509CAReferences + from .x509_attestation import X509Attestation + from .symmetric_key_attestation import SymmetricKeyAttestation + from .attestation_mechanism import AttestationMechanism + from .device_capabilities import DeviceCapabilities + from .metadata import Metadata + from .twin_collection import TwinCollection + from .initial_twin_properties import InitialTwinProperties + from .initial_twin import InitialTwin + from .reprovision_policy import ReprovisionPolicy + from .custom_allocation_definition import CustomAllocationDefinition + from .individual_enrollment import IndividualEnrollment + from .enrollment_group import EnrollmentGroup + from .provisioning_service_error_details import ProvisioningServiceErrorDetails, ProvisioningServiceErrorDetailsException + from .query_specification import QuerySpecification + from .bulk_enrollment_operation import BulkEnrollmentOperation + from .bulk_enrollment_operation_error import BulkEnrollmentOperationError + from .bulk_enrollment_operation_result import BulkEnrollmentOperationResult + from .bulk_enrollment_group_operation import BulkEnrollmentGroupOperation + from .bulk_enrollment_group_operation_error import BulkEnrollmentGroupOperationError + from .bulk_enrollment_group_operation_result import BulkEnrollmentGroupOperationResult + +__all__ = [ + 'DeviceRegistrationState', + 'TpmAttestation', + 'X509CertificateInfo', + 'X509CertificateWithInfo', + 'X509Certificates', + 'X509CAReferences', + 'X509Attestation', + 'SymmetricKeyAttestation', + 'AttestationMechanism', + 'DeviceCapabilities', + 'Metadata', + 'TwinCollection', + 'InitialTwinProperties', + 'InitialTwin', + 'ReprovisionPolicy', + 'CustomAllocationDefinition', + 'IndividualEnrollment', + 'EnrollmentGroup', + 'ProvisioningServiceErrorDetails', 'ProvisioningServiceErrorDetailsException', + 'QuerySpecification', + 'BulkEnrollmentOperation', + 'BulkEnrollmentOperationError', + 'BulkEnrollmentOperationResult', + 'BulkEnrollmentGroupOperation', + 'BulkEnrollmentGroupOperationError', + 'BulkEnrollmentGroupOperationResult', +] diff --git a/azext_iot/sdk/dps/service/models/attestation_mechanism.py b/azext_iot/sdk/dps/service/models/attestation_mechanism.py new file mode 100644 index 000000000..d31e8ab0e --- /dev/null +++ b/azext_iot/sdk/dps/service/models/attestation_mechanism.py @@ -0,0 +1,47 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class AttestationMechanism(Model): + """Attestation mechanism for individualEnrollment as well as enrollmentGroup. + + All required parameters must be populated in order to send to Azure. + + :param type: Required. Attestation Type. Possible values include: 'none', + 'tpm', 'x509', 'symmetricKey' + :type type: str or ~dps.models.enum + :param tpm: TPM attestation method. + :type tpm: ~dps.models.TpmAttestation + :param x509: X509 attestation method. + :type x509: ~dps.models.X509Attestation + :param symmetric_key: Symmetric Key attestation method. + :type symmetric_key: ~dps.models.SymmetricKeyAttestation + """ + + _validation = { + 'type': {'required': True}, + } + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'tpm': {'key': 'tpm', 'type': 'TpmAttestation'}, + 'x509': {'key': 'x509', 'type': 'X509Attestation'}, + 'symmetric_key': {'key': 'symmetricKey', 'type': 'SymmetricKeyAttestation'}, + } + + def __init__(self, **kwargs): + super(AttestationMechanism, self).__init__(**kwargs) + self.type = kwargs.get('type', None) + self.tpm = kwargs.get('tpm', None) + self.x509 = kwargs.get('x509', None) + self.symmetric_key = kwargs.get('symmetric_key', None) diff --git a/azext_iot/sdk/dps/models/attestation_mechanism.py b/azext_iot/sdk/dps/service/models/attestation_mechanism_py3.py similarity index 61% rename from azext_iot/sdk/dps/models/attestation_mechanism.py rename to azext_iot/sdk/dps/service/models/attestation_mechanism_py3.py index dca688476..299119c80 100644 --- a/azext_iot/sdk/dps/models/attestation_mechanism.py +++ b/azext_iot/sdk/dps/service/models/attestation_mechanism_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -11,19 +15,17 @@ class AttestationMechanism(Model): """Attestation mechanism for individualEnrollment as well as enrollmentGroup. - :param type: Attestation Type. Possible values include: 'none', 'tpm', - 'x509', 'symmetricKey' - :type type: str or - ~microsoft.azure.management.provisioningservices.models.enum + All required parameters must be populated in order to send to Azure. + + :param type: Required. Attestation Type. Possible values include: 'none', + 'tpm', 'x509', 'symmetricKey' + :type type: str or ~dps.models.enum :param tpm: TPM attestation method. - :type tpm: - ~microsoft.azure.management.provisioningservices.models.TpmAttestation + :type tpm: ~dps.models.TpmAttestation :param x509: X509 attestation method. - :type x509: - ~microsoft.azure.management.provisioningservices.models.X509Attestation + :type x509: ~dps.models.X509Attestation :param symmetric_key: Symmetric Key attestation method. - :type symmetric_key: - ~microsoft.azure.management.provisioningservices.models.SymmetricKeyAttestation + :type symmetric_key: ~dps.models.SymmetricKeyAttestation """ _validation = { @@ -37,8 +39,8 @@ class AttestationMechanism(Model): 'symmetric_key': {'key': 'symmetricKey', 'type': 'SymmetricKeyAttestation'}, } - def __init__(self, type, tpm=None, x509=None, symmetric_key=None): - super(AttestationMechanism, self).__init__() + def __init__(self, *, type, tpm=None, x509=None, symmetric_key=None, **kwargs) -> None: + super(AttestationMechanism, self).__init__(**kwargs) self.type = type self.tpm = tpm self.x509 = x509 diff --git a/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation.py new file mode 100644 index 000000000..70279cdfb --- /dev/null +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BulkEnrollmentGroupOperation(Model): + """Bulk enrollment operation. + + All required parameters must be populated in order to send to Azure. + + :param enrollment_groups: Required. Enrollment items + :type enrollment_groups: list[~dps.models.EnrollmentGroup] + :param mode: Required. Operation mode. Possible values include: 'create', + 'update', 'updateIfMatchETag', 'delete' + :type mode: str or ~dps.models.enum + """ + + _validation = { + 'enrollment_groups': {'required': True}, + 'mode': {'required': True}, + } + + _attribute_map = { + 'enrollment_groups': {'key': 'enrollmentGroups', 'type': '[EnrollmentGroup]'}, + 'mode': {'key': 'mode', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(BulkEnrollmentGroupOperation, self).__init__(**kwargs) + self.enrollment_groups = kwargs.get('enrollment_groups', None) + self.mode = kwargs.get('mode', None) diff --git a/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_error.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_error.py new file mode 100644 index 000000000..11c583c4d --- /dev/null +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_error.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BulkEnrollmentGroupOperationError(Model): + """Bulk enrollment operation error. + + All required parameters must be populated in order to send to Azure. + + :param enrollment_group_id: Required. Enrollment group id. + :type enrollment_group_id: str + :param error_code: Required. Error code + :type error_code: int + :param error_status: Required. Error status. + :type error_status: str + """ + + _validation = { + 'enrollment_group_id': {'required': True}, + 'error_code': {'required': True}, + 'error_status': {'required': True}, + } + + _attribute_map = { + 'enrollment_group_id': {'key': 'enrollmentGroupId', 'type': 'str'}, + 'error_code': {'key': 'errorCode', 'type': 'int'}, + 'error_status': {'key': 'errorStatus', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(BulkEnrollmentGroupOperationError, self).__init__(**kwargs) + self.enrollment_group_id = kwargs.get('enrollment_group_id', None) + self.error_code = kwargs.get('error_code', None) + self.error_status = kwargs.get('error_status', None) diff --git a/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_error_py3.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_error_py3.py new file mode 100644 index 000000000..09904f613 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_error_py3.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BulkEnrollmentGroupOperationError(Model): + """Bulk enrollment operation error. + + All required parameters must be populated in order to send to Azure. + + :param enrollment_group_id: Required. Enrollment group id. + :type enrollment_group_id: str + :param error_code: Required. Error code + :type error_code: int + :param error_status: Required. Error status. + :type error_status: str + """ + + _validation = { + 'enrollment_group_id': {'required': True}, + 'error_code': {'required': True}, + 'error_status': {'required': True}, + } + + _attribute_map = { + 'enrollment_group_id': {'key': 'enrollmentGroupId', 'type': 'str'}, + 'error_code': {'key': 'errorCode', 'type': 'int'}, + 'error_status': {'key': 'errorStatus', 'type': 'str'}, + } + + def __init__(self, *, enrollment_group_id: str, error_code: int, error_status: str, **kwargs) -> None: + super(BulkEnrollmentGroupOperationError, self).__init__(**kwargs) + self.enrollment_group_id = enrollment_group_id + self.error_code = error_code + self.error_status = error_status diff --git a/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_py3.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_py3.py new file mode 100644 index 000000000..0fd1c67a1 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_py3.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BulkEnrollmentGroupOperation(Model): + """Bulk enrollment operation. + + All required parameters must be populated in order to send to Azure. + + :param enrollment_groups: Required. Enrollment items + :type enrollment_groups: list[~dps.models.EnrollmentGroup] + :param mode: Required. Operation mode. Possible values include: 'create', + 'update', 'updateIfMatchETag', 'delete' + :type mode: str or ~dps.models.enum + """ + + _validation = { + 'enrollment_groups': {'required': True}, + 'mode': {'required': True}, + } + + _attribute_map = { + 'enrollment_groups': {'key': 'enrollmentGroups', 'type': '[EnrollmentGroup]'}, + 'mode': {'key': 'mode', 'type': 'str'}, + } + + def __init__(self, *, enrollment_groups, mode, **kwargs) -> None: + super(BulkEnrollmentGroupOperation, self).__init__(**kwargs) + self.enrollment_groups = enrollment_groups + self.mode = mode diff --git a/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_result.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_result.py new file mode 100644 index 000000000..cd110069b --- /dev/null +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_result.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BulkEnrollmentGroupOperationResult(Model): + """Results of a bulk enrollment group operation. + + All required parameters must be populated in order to send to Azure. + + :param errors: Registration errors + :type errors: list[~dps.models.BulkEnrollmentGroupOperationError] + :param is_successful: Required. Indicates if the operation was successful + in its entirety. + :type is_successful: bool + """ + + _validation = { + 'is_successful': {'required': True}, + } + + _attribute_map = { + 'errors': {'key': 'errors', 'type': '[BulkEnrollmentGroupOperationError]'}, + 'is_successful': {'key': 'isSuccessful', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(BulkEnrollmentGroupOperationResult, self).__init__(**kwargs) + self.errors = kwargs.get('errors', None) + self.is_successful = kwargs.get('is_successful', None) diff --git a/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_result_py3.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_result_py3.py new file mode 100644 index 000000000..d4818887f --- /dev/null +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_group_operation_result_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BulkEnrollmentGroupOperationResult(Model): + """Results of a bulk enrollment group operation. + + All required parameters must be populated in order to send to Azure. + + :param errors: Registration errors + :type errors: list[~dps.models.BulkEnrollmentGroupOperationError] + :param is_successful: Required. Indicates if the operation was successful + in its entirety. + :type is_successful: bool + """ + + _validation = { + 'is_successful': {'required': True}, + } + + _attribute_map = { + 'errors': {'key': 'errors', 'type': '[BulkEnrollmentGroupOperationError]'}, + 'is_successful': {'key': 'isSuccessful', 'type': 'bool'}, + } + + def __init__(self, *, is_successful: bool, errors=None, **kwargs) -> None: + super(BulkEnrollmentGroupOperationResult, self).__init__(**kwargs) + self.errors = errors + self.is_successful = is_successful diff --git a/azext_iot/sdk/dps/service/models/bulk_enrollment_operation.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation.py new file mode 100644 index 000000000..096c0f78b --- /dev/null +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation.py @@ -0,0 +1,40 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BulkEnrollmentOperation(Model): + """Bulk enrollment operation. + + All required parameters must be populated in order to send to Azure. + + :param enrollments: Required. Enrollment items + :type enrollments: list[~dps.models.IndividualEnrollment] + :param mode: Required. Operation mode. Possible values include: 'create', + 'update', 'updateIfMatchETag', 'delete' + :type mode: str or ~dps.models.enum + """ + + _validation = { + 'enrollments': {'required': True}, + 'mode': {'required': True}, + } + + _attribute_map = { + 'enrollments': {'key': 'enrollments', 'type': '[IndividualEnrollment]'}, + 'mode': {'key': 'mode', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(BulkEnrollmentOperation, self).__init__(**kwargs) + self.enrollments = kwargs.get('enrollments', None) + self.mode = kwargs.get('mode', None) diff --git a/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_error.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_error.py new file mode 100644 index 000000000..73c137939 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_error.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BulkEnrollmentOperationError(Model): + """Bulk enrollment operation error. + + All required parameters must be populated in order to send to Azure. + + :param registration_id: Required. Device registration id. + :type registration_id: str + :param error_code: Required. Error code + :type error_code: int + :param error_status: Required. Error status. + :type error_status: str + """ + + _validation = { + 'registration_id': {'required': True}, + 'error_code': {'required': True}, + 'error_status': {'required': True}, + } + + _attribute_map = { + 'registration_id': {'key': 'registrationId', 'type': 'str'}, + 'error_code': {'key': 'errorCode', 'type': 'int'}, + 'error_status': {'key': 'errorStatus', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(BulkEnrollmentOperationError, self).__init__(**kwargs) + self.registration_id = kwargs.get('registration_id', None) + self.error_code = kwargs.get('error_code', None) + self.error_status = kwargs.get('error_status', None) diff --git a/azext_iot/sdk/dps/models/bulk_enrollment_operation_error.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_error_py3.py similarity index 65% rename from azext_iot/sdk/dps/models/bulk_enrollment_operation_error.py rename to azext_iot/sdk/dps/service/models/bulk_enrollment_operation_error_py3.py index c995e899c..a8f798ba5 100644 --- a/azext_iot/sdk/dps/models/bulk_enrollment_operation_error.py +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_error_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -11,11 +15,13 @@ class BulkEnrollmentOperationError(Model): """Bulk enrollment operation error. - :param registration_id: Device registration id. + All required parameters must be populated in order to send to Azure. + + :param registration_id: Required. Device registration id. :type registration_id: str - :param error_code: Error code + :param error_code: Required. Error code :type error_code: int - :param error_status: Error status + :param error_status: Required. Error status. :type error_status: str """ @@ -31,8 +37,8 @@ class BulkEnrollmentOperationError(Model): 'error_status': {'key': 'errorStatus', 'type': 'str'}, } - def __init__(self, registration_id, error_code, error_status): - super(BulkEnrollmentOperationError, self).__init__() + def __init__(self, *, registration_id: str, error_code: int, error_status: str, **kwargs) -> None: + super(BulkEnrollmentOperationError, self).__init__(**kwargs) self.registration_id = registration_id self.error_code = error_code self.error_status = error_status diff --git a/azext_iot/sdk/dps/models/bulk_enrollment_operation.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_py3.py similarity index 52% rename from azext_iot/sdk/dps/models/bulk_enrollment_operation.py rename to azext_iot/sdk/dps/service/models/bulk_enrollment_operation_py3.py index 4a86015e9..7f2c61530 100644 --- a/azext_iot/sdk/dps/models/bulk_enrollment_operation.py +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -9,15 +13,15 @@ class BulkEnrollmentOperation(Model): - """Bulk operation. - - :param enrollments: Enrollment items - :type enrollments: - list[~microsoft.azure.management.provisioningservices.models.IndividualEnrollment] - :param mode: Operation mode. Possible values include: 'create', 'update', - 'updateIfMatchETag', 'delete' - :type mode: str or - ~microsoft.azure.management.provisioningservices.models.enum + """Bulk enrollment operation. + + All required parameters must be populated in order to send to Azure. + + :param enrollments: Required. Enrollment items + :type enrollments: list[~dps.models.IndividualEnrollment] + :param mode: Required. Operation mode. Possible values include: 'create', + 'update', 'updateIfMatchETag', 'delete' + :type mode: str or ~dps.models.enum """ _validation = { @@ -30,7 +34,7 @@ class BulkEnrollmentOperation(Model): 'mode': {'key': 'mode', 'type': 'str'}, } - def __init__(self, enrollments, mode): - super(BulkEnrollmentOperation, self).__init__() + def __init__(self, *, enrollments, mode, **kwargs) -> None: + super(BulkEnrollmentOperation, self).__init__(**kwargs) self.enrollments = enrollments self.mode = mode diff --git a/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_result.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_result.py new file mode 100644 index 000000000..47df002ac --- /dev/null +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_result.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class BulkEnrollmentOperationResult(Model): + """Results of a bulk enrollment operation. + + All required parameters must be populated in order to send to Azure. + + :param errors: Registration errors + :type errors: list[~dps.models.BulkEnrollmentOperationError] + :param is_successful: Required. Indicates if the operation was successful + in its entirety. + :type is_successful: bool + """ + + _validation = { + 'is_successful': {'required': True}, + } + + _attribute_map = { + 'errors': {'key': 'errors', 'type': '[BulkEnrollmentOperationError]'}, + 'is_successful': {'key': 'isSuccessful', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(BulkEnrollmentOperationResult, self).__init__(**kwargs) + self.errors = kwargs.get('errors', None) + self.is_successful = kwargs.get('is_successful', None) diff --git a/azext_iot/sdk/dps/models/bulk_enrollment_operation_result.py b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_result_py3.py similarity index 57% rename from azext_iot/sdk/dps/models/bulk_enrollment_operation_result.py rename to azext_iot/sdk/dps/service/models/bulk_enrollment_operation_result_py3.py index c9f8ab121..cfaccb020 100644 --- a/azext_iot/sdk/dps/models/bulk_enrollment_operation_result.py +++ b/azext_iot/sdk/dps/service/models/bulk_enrollment_operation_result_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -9,14 +13,15 @@ class BulkEnrollmentOperationResult(Model): - """BulkEnrollmentOperationResult. + """Results of a bulk enrollment operation. + + All required parameters must be populated in order to send to Azure. - :param is_successful: Indicates if the operation was successful in its - entirety - :type is_successful: bool :param errors: Registration errors - :type errors: - list[~microsoft.azure.management.provisioningservices.models.BulkEnrollmentOperationError] + :type errors: list[~dps.models.BulkEnrollmentOperationError] + :param is_successful: Required. Indicates if the operation was successful + in its entirety. + :type is_successful: bool """ _validation = { @@ -24,11 +29,11 @@ class BulkEnrollmentOperationResult(Model): } _attribute_map = { - 'is_successful': {'key': 'isSuccessful', 'type': 'bool'}, 'errors': {'key': 'errors', 'type': '[BulkEnrollmentOperationError]'}, + 'is_successful': {'key': 'isSuccessful', 'type': 'bool'}, } - def __init__(self, is_successful, errors=None): - super(BulkEnrollmentOperationResult, self).__init__() - self.is_successful = is_successful + def __init__(self, *, is_successful: bool, errors=None, **kwargs) -> None: + super(BulkEnrollmentOperationResult, self).__init__(**kwargs) self.errors = errors + self.is_successful = is_successful diff --git a/azext_iot/sdk/dps/service/models/custom_allocation_definition.py b/azext_iot/sdk/dps/service/models/custom_allocation_definition.py new file mode 100644 index 000000000..b53f86fe8 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/custom_allocation_definition.py @@ -0,0 +1,42 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class CustomAllocationDefinition(Model): + """Custom allocation definition. + + All required parameters must be populated in order to send to Azure. + + :param webhook_url: Required. The webhook URL used for allocation + requests. + :type webhook_url: str + :param api_version: Required. The API version of the provisioning service + types (such as IndividualEnrollment) sent in the custom allocation + request. Minimum supported version: "2018-09-01-preview". + :type api_version: str + """ + + _validation = { + 'webhook_url': {'required': True}, + 'api_version': {'required': True}, + } + + _attribute_map = { + 'webhook_url': {'key': 'webhookUrl', 'type': 'str'}, + 'api_version': {'key': 'apiVersion', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(CustomAllocationDefinition, self).__init__(**kwargs) + self.webhook_url = kwargs.get('webhook_url', None) + self.api_version = kwargs.get('api_version', None) diff --git a/azext_iot/sdk/dps/models/custom_allocation_definition.py b/azext_iot/sdk/dps/service/models/custom_allocation_definition_py3.py similarity index 55% rename from azext_iot/sdk/dps/models/custom_allocation_definition.py rename to azext_iot/sdk/dps/service/models/custom_allocation_definition_py3.py index 32c7e7a02..4174a9d4a 100644 --- a/azext_iot/sdk/dps/models/custom_allocation_definition.py +++ b/azext_iot/sdk/dps/service/models/custom_allocation_definition_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -11,11 +15,14 @@ class CustomAllocationDefinition(Model): """Custom allocation definition. - :param webhook_url: The webhook URL used for allocation requests. + All required parameters must be populated in order to send to Azure. + + :param webhook_url: Required. The webhook URL used for allocation + requests. :type webhook_url: str - :param api_version: The API version of the provisioning service types - (such as IndividualEnrollment) sent in the custom allocation request. - Supported versions include: "2018-09-01-preview" + :param api_version: Required. The API version of the provisioning service + types (such as IndividualEnrollment) sent in the custom allocation + request. Minimum supported version: "2018-09-01-preview". :type api_version: str """ @@ -29,7 +36,7 @@ class CustomAllocationDefinition(Model): 'api_version': {'key': 'apiVersion', 'type': 'str'}, } - def __init__(self, webhook_url, api_version): - super(CustomAllocationDefinition, self).__init__() + def __init__(self, *, webhook_url: str, api_version: str, **kwargs) -> None: + super(CustomAllocationDefinition, self).__init__(**kwargs) self.webhook_url = webhook_url self.api_version = api_version diff --git a/azext_iot/sdk/dps/service/models/device_capabilities.py b/azext_iot/sdk/dps/service/models/device_capabilities.py new file mode 100644 index 000000000..271a48292 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/device_capabilities.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DeviceCapabilities(Model): + """Device capabilities. + + All required parameters must be populated in order to send to Azure. + + :param iot_edge: Required. If set to true, this device is an IoTEdge + device. Default value: False . + :type iot_edge: bool + """ + + _validation = { + 'iot_edge': {'required': True}, + } + + _attribute_map = { + 'iot_edge': {'key': 'iotEdge', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(DeviceCapabilities, self).__init__(**kwargs) + self.iot_edge = kwargs.get('iot_edge', False) diff --git a/azext_iot/sdk/dps/models/device_capabilities.py b/azext_iot/sdk/dps/service/models/device_capabilities_py3.py similarity index 57% rename from azext_iot/sdk/dps/models/device_capabilities.py rename to azext_iot/sdk/dps/service/models/device_capabilities_py3.py index 5aeed1c3c..55539a142 100644 --- a/azext_iot/sdk/dps/models/device_capabilities.py +++ b/azext_iot/sdk/dps/service/models/device_capabilities_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -11,8 +15,10 @@ class DeviceCapabilities(Model): """Device capabilities. - :param iot_edge: If set to true, this device is an IoTEdge device. Default - value: False . + All required parameters must be populated in order to send to Azure. + + :param iot_edge: Required. If set to true, this device is an IoTEdge + device. Default value: False . :type iot_edge: bool """ @@ -24,6 +30,6 @@ class DeviceCapabilities(Model): 'iot_edge': {'key': 'iotEdge', 'type': 'bool'}, } - def __init__(self, iot_edge=False): - super(DeviceCapabilities, self).__init__() + def __init__(self, *, iot_edge: bool=False, **kwargs) -> None: + super(DeviceCapabilities, self).__init__(**kwargs) self.iot_edge = iot_edge diff --git a/azext_iot/sdk/dps/models/device_registration_state.py b/azext_iot/sdk/dps/service/models/device_registration_state.py similarity index 85% rename from azext_iot/sdk/dps/models/device_registration_state.py rename to azext_iot/sdk/dps/service/models/device_registration_state.py index 235205aa5..9d34f7853 100644 --- a/azext_iot/sdk/dps/models/device_registration_state.py +++ b/azext_iot/sdk/dps/service/models/device_registration_state.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -25,8 +29,7 @@ class DeviceRegistrationState(Model): :vartype device_id: str :ivar status: Enrollment status. Possible values include: 'unassigned', 'assigning', 'assigned', 'failed', 'disabled' - :vartype status: str or - ~microsoft.azure.management.provisioningservices.models.enum + :vartype status: str or ~dps.models.enum :ivar substatus: Substatus for 'Assigned' devices. Possible values include - 'initialAssignment': Device has been assigned to an IoT hub for the first time, 'deviceDataMigrated': Device has been assigned to a different @@ -37,8 +40,7 @@ class DeviceRegistrationState(Model): enrollment. Device data was removed from the previously assigned IoT hub. Possible values include: 'initialAssignment', 'deviceDataMigrated', 'deviceDataReset' - :vartype substatus: str or - ~microsoft.azure.management.provisioningservices.models.enum + :vartype substatus: str or ~dps.models.enum :ivar error_code: Error code. :vartype error_code: int :ivar error_message: Error message. @@ -47,6 +49,9 @@ class DeviceRegistrationState(Model): :vartype last_updated_date_time_utc: datetime :ivar etag: The entity tag associated with the resource. :vartype etag: str + :ivar payload: Custom allocation payload returned from the webhook to the + device. + :vartype payload: object """ _validation = { @@ -60,6 +65,7 @@ class DeviceRegistrationState(Model): 'error_message': {'readonly': True}, 'last_updated_date_time_utc': {'readonly': True}, 'etag': {'readonly': True}, + 'payload': {'readonly': True}, } _attribute_map = { @@ -73,10 +79,11 @@ class DeviceRegistrationState(Model): 'error_message': {'key': 'errorMessage', 'type': 'str'}, 'last_updated_date_time_utc': {'key': 'lastUpdatedDateTimeUtc', 'type': 'iso-8601'}, 'etag': {'key': 'etag', 'type': 'str'}, + 'payload': {'key': 'payload', 'type': 'object'}, } - def __init__(self): - super(DeviceRegistrationState, self).__init__() + def __init__(self, **kwargs): + super(DeviceRegistrationState, self).__init__(**kwargs) self.registration_id = None self.created_date_time_utc = None self.assigned_hub = None @@ -87,3 +94,4 @@ def __init__(self): self.error_message = None self.last_updated_date_time_utc = None self.etag = None + self.payload = None diff --git a/azext_iot/sdk/dps/models/individual_enrollment_registration_state.py b/azext_iot/sdk/dps/service/models/device_registration_state_py3.py similarity index 58% rename from azext_iot/sdk/dps/models/individual_enrollment_registration_state.py rename to azext_iot/sdk/dps/service/models/device_registration_state_py3.py index 46c77709a..dd61aab73 100644 --- a/azext_iot/sdk/dps/models/individual_enrollment_registration_state.py +++ b/azext_iot/sdk/dps/service/models/device_registration_state_py3.py @@ -1,15 +1,19 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. # -------------------------------------------------------------------------- -from .device_registration_state import DeviceRegistrationState +from msrest.serialization import Model -class IndividualEnrollmentRegistrationState(DeviceRegistrationState): - """Current registration status. +class DeviceRegistrationState(Model): + """Device registration state. Variables are only populated by the server, and will be ignored when sending a request. @@ -25,8 +29,7 @@ class IndividualEnrollmentRegistrationState(DeviceRegistrationState): :vartype device_id: str :ivar status: Enrollment status. Possible values include: 'unassigned', 'assigning', 'assigned', 'failed', 'disabled' - :vartype status: str or - ~microsoft.azure.management.provisioningservices.models.enum + :vartype status: str or ~dps.models.enum :ivar substatus: Substatus for 'Assigned' devices. Possible values include - 'initialAssignment': Device has been assigned to an IoT hub for the first time, 'deviceDataMigrated': Device has been assigned to a different @@ -37,8 +40,7 @@ class IndividualEnrollmentRegistrationState(DeviceRegistrationState): enrollment. Device data was removed from the previously assigned IoT hub. Possible values include: 'initialAssignment', 'deviceDataMigrated', 'deviceDataReset' - :vartype substatus: str or - ~microsoft.azure.management.provisioningservices.models.enum + :vartype substatus: str or ~dps.models.enum :ivar error_code: Error code. :vartype error_code: int :ivar error_message: Error message. @@ -47,6 +49,9 @@ class IndividualEnrollmentRegistrationState(DeviceRegistrationState): :vartype last_updated_date_time_utc: datetime :ivar etag: The entity tag associated with the resource. :vartype etag: str + :ivar payload: Custom allocation payload returned from the webhook to the + device. + :vartype payload: object """ _validation = { @@ -60,7 +65,33 @@ class IndividualEnrollmentRegistrationState(DeviceRegistrationState): 'error_message': {'readonly': True}, 'last_updated_date_time_utc': {'readonly': True}, 'etag': {'readonly': True}, + 'payload': {'readonly': True}, } - def __init__(self): - super(IndividualEnrollmentRegistrationState, self).__init__() + _attribute_map = { + 'registration_id': {'key': 'registrationId', 'type': 'str'}, + 'created_date_time_utc': {'key': 'createdDateTimeUtc', 'type': 'iso-8601'}, + 'assigned_hub': {'key': 'assignedHub', 'type': 'str'}, + 'device_id': {'key': 'deviceId', 'type': 'str'}, + 'status': {'key': 'status', 'type': 'str'}, + 'substatus': {'key': 'substatus', 'type': 'str'}, + 'error_code': {'key': 'errorCode', 'type': 'int'}, + 'error_message': {'key': 'errorMessage', 'type': 'str'}, + 'last_updated_date_time_utc': {'key': 'lastUpdatedDateTimeUtc', 'type': 'iso-8601'}, + 'etag': {'key': 'etag', 'type': 'str'}, + 'payload': {'key': 'payload', 'type': 'object'}, + } + + def __init__(self, **kwargs) -> None: + super(DeviceRegistrationState, self).__init__(**kwargs) + self.registration_id = None + self.created_date_time_utc = None + self.assigned_hub = None + self.device_id = None + self.status = None + self.substatus = None + self.error_code = None + self.error_message = None + self.last_updated_date_time_utc = None + self.etag = None + self.payload = None diff --git a/azext_iot/sdk/dps/service/models/enrollment_group.py b/azext_iot/sdk/dps/service/models/enrollment_group.py new file mode 100644 index 000000000..1b19345a4 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/enrollment_group.py @@ -0,0 +1,106 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EnrollmentGroup(Model): + """Enrollment group record. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param enrollment_group_id: Required. Enrollment Group ID. + :type enrollment_group_id: str + :param attestation: Required. Attestation method used by the device. + :type attestation: ~dps.models.AttestationMechanism + :param capabilities: Capabilities of the device. + :type capabilities: ~dps.models.DeviceCapabilities + :param iot_hub_host_name: The Iot Hub host name. + :type iot_hub_host_name: str + :param initial_twin: Initial device twin. + :type initial_twin: ~dps.models.InitialTwin + :param etag: The entity tag associated with the resource. + :type etag: str + :param provisioning_status: The provisioning status. Possible values + include: 'enabled', 'disabled'. Default value: "enabled" . + :type provisioning_status: str or ~dps.models.enum + :param reprovision_policy: The behavior when a device is re-provisioned to + an IoT hub. + :type reprovision_policy: ~dps.models.ReprovisionPolicy + :ivar created_date_time_utc: The DateTime this resource was created. + :vartype created_date_time_utc: datetime + :ivar last_updated_date_time_utc: The DateTime this resource was last + updated. + :vartype last_updated_date_time_utc: datetime + :param allocation_policy: The allocation policy of this resource. This + policy overrides the tenant level allocation policy for this individual + enrollment or enrollment group. Possible values include 'hashed': Linked + IoT hubs are equally likely to have devices provisioned to them, + 'geoLatency': Devices are provisioned to an IoT hub with the lowest + latency to the device.If multiple linked IoT hubs would provide the same + lowest latency, the provisioning service hashes devices across those hubs, + 'static' : Specification of the desired IoT hub in the enrollment list + takes priority over the service-level allocation policy, 'custom': Devices + are provisioned to an IoT hub based on your own custom logic. The + provisioning service passes information about the device to the logic, and + the logic returns the desired IoT hub as well as the desired initial + configuration. We recommend using Azure Functions to host your logic. + Possible values include: 'hashed', 'geoLatency', 'static', 'custom' + :type allocation_policy: str or ~dps.models.enum + :param iot_hubs: The list of names of IoT hubs the device(s) in this + resource can be allocated to. Must be a subset of tenant level list of IoT + hubs. + :type iot_hubs: list[str] + :param custom_allocation_definition: Custom allocation definition. + :type custom_allocation_definition: ~dps.models.CustomAllocationDefinition + """ + + _validation = { + 'enrollment_group_id': {'required': True}, + 'attestation': {'required': True}, + 'created_date_time_utc': {'readonly': True}, + 'last_updated_date_time_utc': {'readonly': True}, + } + + _attribute_map = { + 'enrollment_group_id': {'key': 'enrollmentGroupId', 'type': 'str'}, + 'attestation': {'key': 'attestation', 'type': 'AttestationMechanism'}, + 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, + 'iot_hub_host_name': {'key': 'iotHubHostName', 'type': 'str'}, + 'initial_twin': {'key': 'initialTwin', 'type': 'InitialTwin'}, + 'etag': {'key': 'etag', 'type': 'str'}, + 'provisioning_status': {'key': 'provisioningStatus', 'type': 'str'}, + 'reprovision_policy': {'key': 'reprovisionPolicy', 'type': 'ReprovisionPolicy'}, + 'created_date_time_utc': {'key': 'createdDateTimeUtc', 'type': 'iso-8601'}, + 'last_updated_date_time_utc': {'key': 'lastUpdatedDateTimeUtc', 'type': 'iso-8601'}, + 'allocation_policy': {'key': 'allocationPolicy', 'type': 'str'}, + 'iot_hubs': {'key': 'iotHubs', 'type': '[str]'}, + 'custom_allocation_definition': {'key': 'customAllocationDefinition', 'type': 'CustomAllocationDefinition'}, + } + + def __init__(self, **kwargs): + super(EnrollmentGroup, self).__init__(**kwargs) + self.enrollment_group_id = kwargs.get('enrollment_group_id', None) + self.attestation = kwargs.get('attestation', None) + self.capabilities = kwargs.get('capabilities', None) + self.iot_hub_host_name = kwargs.get('iot_hub_host_name', None) + self.initial_twin = kwargs.get('initial_twin', None) + self.etag = kwargs.get('etag', None) + self.provisioning_status = kwargs.get('provisioning_status', "enabled") + self.reprovision_policy = kwargs.get('reprovision_policy', None) + self.created_date_time_utc = None + self.last_updated_date_time_utc = None + self.allocation_policy = kwargs.get('allocation_policy', None) + self.iot_hubs = kwargs.get('iot_hubs', None) + self.custom_allocation_definition = kwargs.get('custom_allocation_definition', None) diff --git a/azext_iot/sdk/dps/models/enrollment_group.py b/azext_iot/sdk/dps/service/models/enrollment_group_py3.py similarity index 76% rename from azext_iot/sdk/dps/models/enrollment_group.py rename to azext_iot/sdk/dps/service/models/enrollment_group_py3.py index 5acc55954..11dbb59bf 100644 --- a/azext_iot/sdk/dps/models/enrollment_group.py +++ b/azext_iot/sdk/dps/service/models/enrollment_group_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -14,29 +18,26 @@ class EnrollmentGroup(Model): Variables are only populated by the server, and will be ignored when sending a request. - :param enrollment_group_id: Enrollment Group ID. + All required parameters must be populated in order to send to Azure. + + :param enrollment_group_id: Required. Enrollment Group ID. :type enrollment_group_id: str - :param attestation: Attestation method used by the device. - :type attestation: - ~microsoft.azure.management.provisioningservices.models.AttestationMechanism - :param capabilities: Capabilities of the device - :type capabilities: - ~microsoft.azure.management.provisioningservices.models.DeviceCapabilities + :param attestation: Required. Attestation method used by the device. + :type attestation: ~dps.models.AttestationMechanism + :param capabilities: Capabilities of the device. + :type capabilities: ~dps.models.DeviceCapabilities :param iot_hub_host_name: The Iot Hub host name. :type iot_hub_host_name: str :param initial_twin: Initial device twin. - :type initial_twin: - ~microsoft.azure.management.provisioningservices.models.InitialTwin + :type initial_twin: ~dps.models.InitialTwin :param etag: The entity tag associated with the resource. :type etag: str :param provisioning_status: The provisioning status. Possible values include: 'enabled', 'disabled'. Default value: "enabled" . - :type provisioning_status: str or - ~microsoft.azure.management.provisioningservices.models.enum + :type provisioning_status: str or ~dps.models.enum :param reprovision_policy: The behavior when a device is re-provisioned to an IoT hub. - :type reprovision_policy: - ~microsoft.azure.management.provisioningservices.models.ReprovisionPolicy + :type reprovision_policy: ~dps.models.ReprovisionPolicy :ivar created_date_time_utc: The DateTime this resource was created. :vartype created_date_time_utc: datetime :ivar last_updated_date_time_utc: The DateTime this resource was last @@ -56,15 +57,13 @@ class EnrollmentGroup(Model): the logic returns the desired IoT hub as well as the desired initial configuration. We recommend using Azure Functions to host your logic. Possible values include: 'hashed', 'geoLatency', 'static', 'custom' - :type allocation_policy: str or - ~microsoft.azure.management.provisioningservices.models.enum + :type allocation_policy: str or ~dps.models.enum :param iot_hubs: The list of names of IoT hubs the device(s) in this resource can be allocated to. Must be a subset of tenant level list of IoT hubs. :type iot_hubs: list[str] :param custom_allocation_definition: Custom allocation definition. - :type custom_allocation_definition: - ~microsoft.azure.management.provisioningservices.models.CustomAllocationDefinition + :type custom_allocation_definition: ~dps.models.CustomAllocationDefinition """ _validation = { @@ -75,9 +74,9 @@ class EnrollmentGroup(Model): } _attribute_map = { - 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, # @digimaun - added capabilities custom 'enrollment_group_id': {'key': 'enrollmentGroupId', 'type': 'str'}, 'attestation': {'key': 'attestation', 'type': 'AttestationMechanism'}, + 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, 'iot_hub_host_name': {'key': 'iotHubHostName', 'type': 'str'}, 'initial_twin': {'key': 'initialTwin', 'type': 'InitialTwin'}, 'etag': {'key': 'etag', 'type': 'str'}, @@ -90,11 +89,11 @@ class EnrollmentGroup(Model): 'custom_allocation_definition': {'key': 'customAllocationDefinition', 'type': 'CustomAllocationDefinition'}, } - def __init__(self, enrollment_group_id, attestation, capabilities=None, iot_hub_host_name=None, initial_twin=None, etag=None, provisioning_status="enabled", reprovision_policy=None, allocation_policy=None, iot_hubs=None, custom_allocation_definition=None): - super(EnrollmentGroup, self).__init__() - self.capabilities = capabilities # @digmaun - added capabilities custom + def __init__(self, *, enrollment_group_id: str, attestation, capabilities=None, iot_hub_host_name: str=None, initial_twin=None, etag: str=None, provisioning_status="enabled", reprovision_policy=None, allocation_policy=None, iot_hubs=None, custom_allocation_definition=None, **kwargs) -> None: + super(EnrollmentGroup, self).__init__(**kwargs) self.enrollment_group_id = enrollment_group_id self.attestation = attestation + self.capabilities = capabilities self.iot_hub_host_name = iot_hub_host_name self.initial_twin = initial_twin self.etag = etag diff --git a/azext_iot/sdk/dps/service/models/individual_enrollment.py b/azext_iot/sdk/dps/service/models/individual_enrollment.py new file mode 100644 index 000000000..5fe9a325a --- /dev/null +++ b/azext_iot/sdk/dps/service/models/individual_enrollment.py @@ -0,0 +1,116 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class IndividualEnrollment(Model): + """The device enrollment record. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param registration_id: Required. The registration ID is alphanumeric, + lowercase, and may contain hyphens. + :type registration_id: str + :param device_id: Desired IoT Hub device ID (optional). + :type device_id: str + :ivar registration_state: Current registration status. + :vartype registration_state: ~dps.models.DeviceRegistrationState + :param attestation: Required. Attestation method used by the device. + :type attestation: ~dps.models.AttestationMechanism + :param capabilities: Capabilities of the device. + :type capabilities: ~dps.models.DeviceCapabilities + :param iot_hub_host_name: The Iot Hub host name. + :type iot_hub_host_name: str + :param initial_twin: Initial device twin. + :type initial_twin: ~dps.models.InitialTwin + :param etag: The entity tag associated with the resource. + :type etag: str + :param provisioning_status: The provisioning status. Possible values + include: 'enabled', 'disabled'. Default value: "enabled" . + :type provisioning_status: str or ~dps.models.enum + :param reprovision_policy: The behavior when a device is re-provisioned to + an IoT hub. + :type reprovision_policy: ~dps.models.ReprovisionPolicy + :ivar created_date_time_utc: The DateTime this resource was created. + :vartype created_date_time_utc: datetime + :ivar last_updated_date_time_utc: The DateTime this resource was last + updated. + :vartype last_updated_date_time_utc: datetime + :param allocation_policy: The allocation policy of this resource. This + policy overrides the tenant level allocation policy for this individual + enrollment or enrollment group. Possible values include 'hashed': Linked + IoT hubs are equally likely to have devices provisioned to them, + 'geoLatency': Devices are provisioned to an IoT hub with the lowest + latency to the device.If multiple linked IoT hubs would provide the same + lowest latency, the provisioning service hashes devices across those hubs, + 'static' : Specification of the desired IoT hub in the enrollment list + takes priority over the service-level allocation policy, 'custom': Devices + are provisioned to an IoT hub based on your own custom logic. The + provisioning service passes information about the device to the logic, and + the logic returns the desired IoT hub as well as the desired initial + configuration. We recommend using Azure Functions to host your logic. + Possible values include: 'hashed', 'geoLatency', 'static', 'custom' + :type allocation_policy: str or ~dps.models.enum + :param iot_hubs: The list of names of IoT hubs the device(s) in this + resource can be allocated to. Must be a subset of tenant level list of IoT + hubs. + :type iot_hubs: list[str] + :param custom_allocation_definition: Custom allocation definition. + :type custom_allocation_definition: ~dps.models.CustomAllocationDefinition + """ + + _validation = { + 'registration_id': {'required': True}, + 'registration_state': {'readonly': True}, + 'attestation': {'required': True}, + 'created_date_time_utc': {'readonly': True}, + 'last_updated_date_time_utc': {'readonly': True}, + } + + _attribute_map = { + 'registration_id': {'key': 'registrationId', 'type': 'str'}, + 'device_id': {'key': 'deviceId', 'type': 'str'}, + 'registration_state': {'key': 'registrationState', 'type': 'DeviceRegistrationState'}, + 'attestation': {'key': 'attestation', 'type': 'AttestationMechanism'}, + 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, + 'iot_hub_host_name': {'key': 'iotHubHostName', 'type': 'str'}, + 'initial_twin': {'key': 'initialTwin', 'type': 'InitialTwin'}, + 'etag': {'key': 'etag', 'type': 'str'}, + 'provisioning_status': {'key': 'provisioningStatus', 'type': 'str'}, + 'reprovision_policy': {'key': 'reprovisionPolicy', 'type': 'ReprovisionPolicy'}, + 'created_date_time_utc': {'key': 'createdDateTimeUtc', 'type': 'iso-8601'}, + 'last_updated_date_time_utc': {'key': 'lastUpdatedDateTimeUtc', 'type': 'iso-8601'}, + 'allocation_policy': {'key': 'allocationPolicy', 'type': 'str'}, + 'iot_hubs': {'key': 'iotHubs', 'type': '[str]'}, + 'custom_allocation_definition': {'key': 'customAllocationDefinition', 'type': 'CustomAllocationDefinition'}, + } + + def __init__(self, **kwargs): + super(IndividualEnrollment, self).__init__(**kwargs) + self.registration_id = kwargs.get('registration_id', None) + self.device_id = kwargs.get('device_id', None) + self.registration_state = None + self.attestation = kwargs.get('attestation', None) + self.capabilities = kwargs.get('capabilities', None) + self.iot_hub_host_name = kwargs.get('iot_hub_host_name', None) + self.initial_twin = kwargs.get('initial_twin', None) + self.etag = kwargs.get('etag', None) + self.provisioning_status = kwargs.get('provisioning_status', "enabled") + self.reprovision_policy = kwargs.get('reprovision_policy', None) + self.created_date_time_utc = None + self.last_updated_date_time_utc = None + self.allocation_policy = kwargs.get('allocation_policy', None) + self.iot_hubs = kwargs.get('iot_hubs', None) + self.custom_allocation_definition = kwargs.get('custom_allocation_definition', None) diff --git a/azext_iot/sdk/dps/models/individual_enrollment.py b/azext_iot/sdk/dps/service/models/individual_enrollment_py3.py similarity index 76% rename from azext_iot/sdk/dps/models/individual_enrollment.py rename to azext_iot/sdk/dps/service/models/individual_enrollment_py3.py index bf4f4e6e2..aca250416 100644 --- a/azext_iot/sdk/dps/models/individual_enrollment.py +++ b/azext_iot/sdk/dps/service/models/individual_enrollment_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -14,35 +18,31 @@ class IndividualEnrollment(Model): Variables are only populated by the server, and will be ignored when sending a request. - :param capabilities: Capabilities of the device - :type capabilities: - ~microsoft.azure.management.provisioningservices.models.DeviceCapabilities - :param registration_id: The registration ID is alphanumeric, lowercase, - and may contain hyphens. + All required parameters must be populated in order to send to Azure. + + :param registration_id: Required. The registration ID is alphanumeric, + lowercase, and may contain hyphens. :type registration_id: str :param device_id: Desired IoT Hub device ID (optional). :type device_id: str :ivar registration_state: Current registration status. - :vartype registration_state: - ~microsoft.azure.management.provisioningservices.models.IndividualEnrollmentRegistrationState - :param attestation: Attestation method used by the device. - :type attestation: - ~microsoft.azure.management.provisioningservices.models.AttestationMechanism + :vartype registration_state: ~dps.models.DeviceRegistrationState + :param attestation: Required. Attestation method used by the device. + :type attestation: ~dps.models.AttestationMechanism + :param capabilities: Capabilities of the device. + :type capabilities: ~dps.models.DeviceCapabilities :param iot_hub_host_name: The Iot Hub host name. :type iot_hub_host_name: str :param initial_twin: Initial device twin. - :type initial_twin: - ~microsoft.azure.management.provisioningservices.models.InitialTwin + :type initial_twin: ~dps.models.InitialTwin :param etag: The entity tag associated with the resource. :type etag: str :param provisioning_status: The provisioning status. Possible values include: 'enabled', 'disabled'. Default value: "enabled" . - :type provisioning_status: str or - ~microsoft.azure.management.provisioningservices.models.enum + :type provisioning_status: str or ~dps.models.enum :param reprovision_policy: The behavior when a device is re-provisioned to an IoT hub. - :type reprovision_policy: - ~microsoft.azure.management.provisioningservices.models.ReprovisionPolicy + :type reprovision_policy: ~dps.models.ReprovisionPolicy :ivar created_date_time_utc: The DateTime this resource was created. :vartype created_date_time_utc: datetime :ivar last_updated_date_time_utc: The DateTime this resource was last @@ -62,15 +62,13 @@ class IndividualEnrollment(Model): the logic returns the desired IoT hub as well as the desired initial configuration. We recommend using Azure Functions to host your logic. Possible values include: 'hashed', 'geoLatency', 'static', 'custom' - :type allocation_policy: str or - ~microsoft.azure.management.provisioningservices.models.enum + :type allocation_policy: str or ~dps.models.enum :param iot_hubs: The list of names of IoT hubs the device(s) in this resource can be allocated to. Must be a subset of tenant level list of IoT hubs. :type iot_hubs: list[str] :param custom_allocation_definition: Custom allocation definition. - :type custom_allocation_definition: - ~microsoft.azure.management.provisioningservices.models.CustomAllocationDefinition + :type custom_allocation_definition: ~dps.models.CustomAllocationDefinition """ _validation = { @@ -82,11 +80,11 @@ class IndividualEnrollment(Model): } _attribute_map = { - 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, 'registration_id': {'key': 'registrationId', 'type': 'str'}, 'device_id': {'key': 'deviceId', 'type': 'str'}, - 'registration_state': {'key': 'registrationState', 'type': 'IndividualEnrollmentRegistrationState'}, + 'registration_state': {'key': 'registrationState', 'type': 'DeviceRegistrationState'}, 'attestation': {'key': 'attestation', 'type': 'AttestationMechanism'}, + 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, 'iot_hub_host_name': {'key': 'iotHubHostName', 'type': 'str'}, 'initial_twin': {'key': 'initialTwin', 'type': 'InitialTwin'}, 'etag': {'key': 'etag', 'type': 'str'}, @@ -99,13 +97,13 @@ class IndividualEnrollment(Model): 'custom_allocation_definition': {'key': 'customAllocationDefinition', 'type': 'CustomAllocationDefinition'}, } - def __init__(self, registration_id, attestation, capabilities=None, device_id=None, iot_hub_host_name=None, initial_twin=None, etag=None, provisioning_status="enabled", reprovision_policy=None, allocation_policy=None, iot_hubs=None, custom_allocation_definition=None): - super(IndividualEnrollment, self).__init__() - self.capabilities = capabilities + def __init__(self, *, registration_id: str, attestation, device_id: str=None, capabilities=None, iot_hub_host_name: str=None, initial_twin=None, etag: str=None, provisioning_status="enabled", reprovision_policy=None, allocation_policy=None, iot_hubs=None, custom_allocation_definition=None, **kwargs) -> None: + super(IndividualEnrollment, self).__init__(**kwargs) self.registration_id = registration_id self.device_id = device_id self.registration_state = None self.attestation = attestation + self.capabilities = capabilities self.iot_hub_host_name = iot_hub_host_name self.initial_twin = initial_twin self.etag = etag diff --git a/azext_iot/sdk/dps/service/models/initial_twin.py b/azext_iot/sdk/dps/service/models/initial_twin.py new file mode 100644 index 000000000..069b74593 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/initial_twin.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class InitialTwin(Model): + """Initial device twin. Contains a subset of the properties of Twin. + + :param tags: Twin tags. + :type tags: ~dps.models.TwinCollection + :param properties: Twin desired properties. + :type properties: ~dps.models.InitialTwinProperties + """ + + _attribute_map = { + 'tags': {'key': 'tags', 'type': 'TwinCollection'}, + 'properties': {'key': 'properties', 'type': 'InitialTwinProperties'}, + } + + def __init__(self, **kwargs): + super(InitialTwin, self).__init__(**kwargs) + self.tags = kwargs.get('tags', None) + self.properties = kwargs.get('properties', None) diff --git a/azext_iot/sdk/dps/service/models/initial_twin_properties.py b/azext_iot/sdk/dps/service/models/initial_twin_properties.py new file mode 100644 index 000000000..dead116b8 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/initial_twin_properties.py @@ -0,0 +1,28 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class InitialTwinProperties(Model): + """Represents the initial properties that will be set on the device twin. + + :param desired: Gets and sets the InitialTwin desired properties. + :type desired: ~dps.models.TwinCollection + """ + + _attribute_map = { + 'desired': {'key': 'desired', 'type': 'TwinCollection'}, + } + + def __init__(self, **kwargs): + super(InitialTwinProperties, self).__init__(**kwargs) + self.desired = kwargs.get('desired', None) diff --git a/azext_iot/sdk/dps/models/initial_twin_properties.py b/azext_iot/sdk/dps/service/models/initial_twin_properties_py3.py similarity index 59% rename from azext_iot/sdk/dps/models/initial_twin_properties.py rename to azext_iot/sdk/dps/service/models/initial_twin_properties_py3.py index 955e82d8a..b3b86445e 100644 --- a/azext_iot/sdk/dps/models/initial_twin_properties.py +++ b/azext_iot/sdk/dps/service/models/initial_twin_properties_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -9,17 +13,16 @@ class InitialTwinProperties(Model): - """InitialTwinProperties. + """Represents the initial properties that will be set on the device twin. :param desired: Gets and sets the InitialTwin desired properties. - :type desired: - ~microsoft.azure.management.provisioningservices.models.TwinCollection + :type desired: ~dps.models.TwinCollection """ _attribute_map = { 'desired': {'key': 'desired', 'type': 'TwinCollection'}, } - def __init__(self, desired=None): - super(InitialTwinProperties, self).__init__() + def __init__(self, *, desired=None, **kwargs) -> None: + super(InitialTwinProperties, self).__init__(**kwargs) self.desired = desired diff --git a/azext_iot/sdk/dps/models/initial_twin.py b/azext_iot/sdk/dps/service/models/initial_twin_py3.py similarity index 66% rename from azext_iot/sdk/dps/models/initial_twin.py rename to azext_iot/sdk/dps/service/models/initial_twin_py3.py index e059973e6..91019fb75 100644 --- a/azext_iot/sdk/dps/models/initial_twin.py +++ b/azext_iot/sdk/dps/service/models/initial_twin_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -12,11 +16,9 @@ class InitialTwin(Model): """Initial device twin. Contains a subset of the properties of Twin. :param tags: Twin tags. - :type tags: - ~microsoft.azure.management.provisioningservices.models.TwinCollection + :type tags: ~dps.models.TwinCollection :param properties: Twin desired properties. - :type properties: - ~microsoft.azure.management.provisioningservices.models.InitialTwinProperties + :type properties: ~dps.models.InitialTwinProperties """ _attribute_map = { @@ -24,7 +26,7 @@ class InitialTwin(Model): 'properties': {'key': 'properties', 'type': 'InitialTwinProperties'}, } - def __init__(self, tags=None, properties=None): - super(InitialTwin, self).__init__() + def __init__(self, *, tags=None, properties=None, **kwargs) -> None: + super(InitialTwin, self).__init__(**kwargs) self.tags = tags self.properties = properties diff --git a/azext_iot/sdk/dps/service/models/metadata.py b/azext_iot/sdk/dps/service/models/metadata.py new file mode 100644 index 000000000..560fdf217 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/metadata.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class Metadata(Model): + """Metadata for the TwinCollection. + + :param last_updated: Last time the TwinCollection was updated + :type last_updated: datetime + :param last_updated_version: This SHOULD be null for Reported properties + metadata and MUST not be null for Desired properties metadata. + :type last_updated_version: long + """ + + _attribute_map = { + 'last_updated': {'key': 'lastUpdated', 'type': 'iso-8601'}, + 'last_updated_version': {'key': 'lastUpdatedVersion', 'type': 'long'}, + } + + def __init__(self, **kwargs): + super(Metadata, self).__init__(**kwargs) + self.last_updated = kwargs.get('last_updated', None) + self.last_updated_version = kwargs.get('last_updated_version', None) diff --git a/azext_iot/sdk/dps/models/metadata.py b/azext_iot/sdk/dps/service/models/metadata_py3.py similarity index 68% rename from azext_iot/sdk/dps/models/metadata.py rename to azext_iot/sdk/dps/service/models/metadata_py3.py index 5ade4a1ec..98715ac40 100644 --- a/azext_iot/sdk/dps/models/metadata.py +++ b/azext_iot/sdk/dps/service/models/metadata_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -9,9 +13,9 @@ class Metadata(Model): - """Metadata. + """Metadata for the TwinCollection. - :param last_updated: + :param last_updated: Last time the TwinCollection was updated :type last_updated: datetime :param last_updated_version: This SHOULD be null for Reported properties metadata and MUST not be null for Desired properties metadata. @@ -23,7 +27,7 @@ class Metadata(Model): 'last_updated_version': {'key': 'lastUpdatedVersion', 'type': 'long'}, } - def __init__(self, last_updated=None, last_updated_version=None): - super(Metadata, self).__init__() + def __init__(self, *, last_updated=None, last_updated_version: int=None, **kwargs) -> None: + super(Metadata, self).__init__(**kwargs) self.last_updated = last_updated self.last_updated_version = last_updated_version diff --git a/azext_iot/sdk/dps/service/models/provisioning_service_error_details.py b/azext_iot/sdk/dps/service/models/provisioning_service_error_details.py new file mode 100644 index 000000000..e58db7faf --- /dev/null +++ b/azext_iot/sdk/dps/service/models/provisioning_service_error_details.py @@ -0,0 +1,58 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class ProvisioningServiceErrorDetails(Model): + """Contains the properties of an error returned by the Azure IoT Hub + Provisioning Service. + + :param error_code: + :type error_code: int + :param tracking_id: + :type tracking_id: str + :param message: + :type message: str + :param info: + :type info: dict[str, str] + :param timestamp_utc: + :type timestamp_utc: datetime + """ + + _attribute_map = { + 'error_code': {'key': 'errorCode', 'type': 'int'}, + 'tracking_id': {'key': 'trackingId', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'info': {'key': 'info', 'type': '{str}'}, + 'timestamp_utc': {'key': 'timestampUtc', 'type': 'iso-8601'}, + } + + def __init__(self, **kwargs): + super(ProvisioningServiceErrorDetails, self).__init__(**kwargs) + self.error_code = kwargs.get('error_code', None) + self.tracking_id = kwargs.get('tracking_id', None) + self.message = kwargs.get('message', None) + self.info = kwargs.get('info', None) + self.timestamp_utc = kwargs.get('timestamp_utc', None) + + +class ProvisioningServiceErrorDetailsException(HttpOperationError): + """Server responsed with exception of type: 'ProvisioningServiceErrorDetails'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(ProvisioningServiceErrorDetailsException, self).__init__(deserialize, response, 'ProvisioningServiceErrorDetails', *args) diff --git a/azext_iot/sdk/dps/models/provisioning_service_error_details.py b/azext_iot/sdk/dps/service/models/provisioning_service_error_details_py3.py similarity index 77% rename from azext_iot/sdk/dps/models/provisioning_service_error_details.py rename to azext_iot/sdk/dps/service/models/provisioning_service_error_details_py3.py index d1e6743bd..2c1411312 100644 --- a/azext_iot/sdk/dps/models/provisioning_service_error_details.py +++ b/azext_iot/sdk/dps/service/models/provisioning_service_error_details_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -10,7 +14,8 @@ class ProvisioningServiceErrorDetails(Model): - """ProvisioningServiceErrorDetails. + """Contains the properties of an error returned by the Azure IoT Hub + Provisioning Service. :param error_code: :type error_code: int @@ -32,8 +37,8 @@ class ProvisioningServiceErrorDetails(Model): 'timestamp_utc': {'key': 'timestampUtc', 'type': 'iso-8601'}, } - def __init__(self, error_code=None, tracking_id=None, message=None, info=None, timestamp_utc=None): - super(ProvisioningServiceErrorDetails, self).__init__() + def __init__(self, *, error_code: int=None, tracking_id: str=None, message: str=None, info=None, timestamp_utc=None, **kwargs) -> None: + super(ProvisioningServiceErrorDetails, self).__init__(**kwargs) self.error_code = error_code self.tracking_id = tracking_id self.message = message diff --git a/azext_iot/sdk/dps/service/models/query_specification.py b/azext_iot/sdk/dps/service/models/query_specification.py new file mode 100644 index 000000000..378002dd4 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/query_specification.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class QuerySpecification(Model): + """A Json query request. + + All required parameters must be populated in order to send to Azure. + + :param query: Required. The query. + :type query: str + """ + + _validation = { + 'query': {'required': True}, + } + + _attribute_map = { + 'query': {'key': 'query', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(QuerySpecification, self).__init__(**kwargs) + self.query = kwargs.get('query', None) diff --git a/azext_iot/sdk/dps/models/query_specification.py b/azext_iot/sdk/dps/service/models/query_specification_py3.py similarity index 61% rename from azext_iot/sdk/dps/models/query_specification.py rename to azext_iot/sdk/dps/service/models/query_specification_py3.py index 990dbf1e3..290bd23a2 100644 --- a/azext_iot/sdk/dps/models/query_specification.py +++ b/azext_iot/sdk/dps/service/models/query_specification_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -11,7 +15,9 @@ class QuerySpecification(Model): """A Json query request. - :param query: The query. + All required parameters must be populated in order to send to Azure. + + :param query: Required. The query. :type query: str """ @@ -23,6 +29,6 @@ class QuerySpecification(Model): 'query': {'key': 'query', 'type': 'str'}, } - def __init__(self, query): - super(QuerySpecification, self).__init__() + def __init__(self, *, query: str, **kwargs) -> None: + super(QuerySpecification, self).__init__(**kwargs) self.query = query diff --git a/azext_iot/sdk/dps/service/models/reprovision_policy.py b/azext_iot/sdk/dps/service/models/reprovision_policy.py new file mode 100644 index 000000000..92e777bf6 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/reprovision_policy.py @@ -0,0 +1,48 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ReprovisionPolicy(Model): + """The behavior of the service when a device is re-provisioned to an IoT hub. + + All required parameters must be populated in order to send to Azure. + + :param update_hub_assignment: Required. When set to true (default), the + Device Provisioning Service will evaluate the device's IoT Hub assignment + and update it if necessary for any provisioning requests beyond the first + from a given device. If set to false, the device will stay assigned to its + current IoT hub. Default value: True . + :type update_hub_assignment: bool + :param migrate_device_data: Required. When set to true (default), the + Device Provisioning Service will migrate the device's data (twin, device + capabilities, and device ID) from one IoT hub to another during an IoT hub + assignment update. If set to false, the Device Provisioning Service will + reset the device's data to the initial desired configuration stored in the + corresponding enrollment list. Default value: True . + :type migrate_device_data: bool + """ + + _validation = { + 'update_hub_assignment': {'required': True}, + 'migrate_device_data': {'required': True}, + } + + _attribute_map = { + 'update_hub_assignment': {'key': 'updateHubAssignment', 'type': 'bool'}, + 'migrate_device_data': {'key': 'migrateDeviceData', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(ReprovisionPolicy, self).__init__(**kwargs) + self.update_hub_assignment = kwargs.get('update_hub_assignment', True) + self.migrate_device_data = kwargs.get('migrate_device_data', True) diff --git a/azext_iot/sdk/dps/models/reprovision_policy.py b/azext_iot/sdk/dps/service/models/reprovision_policy_py3.py similarity index 60% rename from azext_iot/sdk/dps/models/reprovision_policy.py rename to azext_iot/sdk/dps/service/models/reprovision_policy_py3.py index dafaa1ee0..3a1403f2e 100644 --- a/azext_iot/sdk/dps/models/reprovision_policy.py +++ b/azext_iot/sdk/dps/service/models/reprovision_policy_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -11,14 +15,16 @@ class ReprovisionPolicy(Model): """The behavior of the service when a device is re-provisioned to an IoT hub. - :param update_hub_assignment: When set to true (default), the Device - Provisioning Service will evaluate the device's IoT Hub assignment and - update it if necessary for any provisioning requests beyond the first from - a given device. If set to false, the device will stay assigned to its + All required parameters must be populated in order to send to Azure. + + :param update_hub_assignment: Required. When set to true (default), the + Device Provisioning Service will evaluate the device's IoT Hub assignment + and update it if necessary for any provisioning requests beyond the first + from a given device. If set to false, the device will stay assigned to its current IoT hub. Default value: True . :type update_hub_assignment: bool - :param migrate_device_data: When set to true (default), the Device - Provisioning Service will migrate the device's data (twin, device + :param migrate_device_data: Required. When set to true (default), the + Device Provisioning Service will migrate the device's data (twin, device capabilities, and device ID) from one IoT hub to another during an IoT hub assignment update. If set to false, the Device Provisioning Service will reset the device's data to the initial desired configuration stored in the @@ -36,7 +42,7 @@ class ReprovisionPolicy(Model): 'migrate_device_data': {'key': 'migrateDeviceData', 'type': 'bool'}, } - def __init__(self, update_hub_assignment=True, migrate_device_data=True): - super(ReprovisionPolicy, self).__init__() + def __init__(self, *, update_hub_assignment: bool=True, migrate_device_data: bool=True, **kwargs) -> None: + super(ReprovisionPolicy, self).__init__(**kwargs) self.update_hub_assignment = update_hub_assignment self.migrate_device_data = migrate_device_data diff --git a/azext_iot/sdk/dps/service/models/symmetric_key_attestation.py b/azext_iot/sdk/dps/service/models/symmetric_key_attestation.py new file mode 100644 index 000000000..6e94284c9 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/symmetric_key_attestation.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class SymmetricKeyAttestation(Model): + """Attestation via SymmetricKey. + + :param primary_key: Primary symmetric key. + :type primary_key: str + :param secondary_key: Secondary symmetric key. + :type secondary_key: str + """ + + _attribute_map = { + 'primary_key': {'key': 'primaryKey', 'type': 'str'}, + 'secondary_key': {'key': 'secondaryKey', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(SymmetricKeyAttestation, self).__init__(**kwargs) + self.primary_key = kwargs.get('primary_key', None) + self.secondary_key = kwargs.get('secondary_key', None) diff --git a/azext_iot/sdk/dps/models/symmetric_key_attestation.py b/azext_iot/sdk/dps/service/models/symmetric_key_attestation_py3.py similarity index 72% rename from azext_iot/sdk/dps/models/symmetric_key_attestation.py rename to azext_iot/sdk/dps/service/models/symmetric_key_attestation_py3.py index 28a1ef96a..b2efe3ce4 100644 --- a/azext_iot/sdk/dps/models/symmetric_key_attestation.py +++ b/azext_iot/sdk/dps/service/models/symmetric_key_attestation_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -22,7 +26,7 @@ class SymmetricKeyAttestation(Model): 'secondary_key': {'key': 'secondaryKey', 'type': 'str'}, } - def __init__(self, primary_key=None, secondary_key=None): - super(SymmetricKeyAttestation, self).__init__() + def __init__(self, *, primary_key: str=None, secondary_key: str=None, **kwargs) -> None: + super(SymmetricKeyAttestation, self).__init__(**kwargs) self.primary_key = primary_key self.secondary_key = secondary_key diff --git a/azext_iot/sdk/dps/service/models/tpm_attestation.py b/azext_iot/sdk/dps/service/models/tpm_attestation.py new file mode 100644 index 000000000..1643e80c9 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/tpm_attestation.py @@ -0,0 +1,38 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TpmAttestation(Model): + """Attestation via TPM. + + All required parameters must be populated in order to send to Azure. + + :param endorsement_key: Required. + :type endorsement_key: str + :param storage_root_key: + :type storage_root_key: str + """ + + _validation = { + 'endorsement_key': {'required': True}, + } + + _attribute_map = { + 'endorsement_key': {'key': 'endorsementKey', 'type': 'str'}, + 'storage_root_key': {'key': 'storageRootKey', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(TpmAttestation, self).__init__(**kwargs) + self.endorsement_key = kwargs.get('endorsement_key', None) + self.storage_root_key = kwargs.get('storage_root_key', None) diff --git a/azext_iot/sdk/dps/models/tpm_attestation.py b/azext_iot/sdk/dps/service/models/tpm_attestation_py3.py similarity index 66% rename from azext_iot/sdk/dps/models/tpm_attestation.py rename to azext_iot/sdk/dps/service/models/tpm_attestation_py3.py index aaf7e8b9f..648b364cc 100644 --- a/azext_iot/sdk/dps/models/tpm_attestation.py +++ b/azext_iot/sdk/dps/service/models/tpm_attestation_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -11,7 +15,9 @@ class TpmAttestation(Model): """Attestation via TPM. - :param endorsement_key: + All required parameters must be populated in order to send to Azure. + + :param endorsement_key: Required. :type endorsement_key: str :param storage_root_key: :type storage_root_key: str @@ -26,7 +32,7 @@ class TpmAttestation(Model): 'storage_root_key': {'key': 'storageRootKey', 'type': 'str'}, } - def __init__(self, endorsement_key, storage_root_key=None): - super(TpmAttestation, self).__init__() + def __init__(self, *, endorsement_key: str, storage_root_key: str=None, **kwargs) -> None: + super(TpmAttestation, self).__init__(**kwargs) self.endorsement_key = endorsement_key self.storage_root_key = storage_root_key diff --git a/azext_iot/sdk/dps/service/models/twin_collection.py b/azext_iot/sdk/dps/service/models/twin_collection.py new file mode 100644 index 000000000..3ead028fc --- /dev/null +++ b/azext_iot/sdk/dps/service/models/twin_collection.py @@ -0,0 +1,41 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class TwinCollection(Model): + """Represents a collection of properties within a Twin. + + :param additional_properties: Unmatched properties from the message are + deserialized this collection + :type additional_properties: dict[str, object] + :param version: Version of the TwinCollection + :type version: long + :param count: Number of properties in the TwinCollection + :type count: int + :param metadata: Metadata for the TwinCollection + :type metadata: ~dps.models.Metadata + """ + + _attribute_map = { + 'additional_properties': {'key': '', 'type': '{object}'}, + 'version': {'key': 'version', 'type': 'long'}, + 'count': {'key': 'count', 'type': 'int'}, + 'metadata': {'key': 'metadata', 'type': 'Metadata'}, + } + + def __init__(self, **kwargs): + super(TwinCollection, self).__init__(**kwargs) + self.additional_properties = kwargs.get('additional_properties', None) + self.version = kwargs.get('version', None) + self.count = kwargs.get('count', None) + self.metadata = kwargs.get('metadata', None) diff --git a/azext_iot/sdk/dps/models/twin_collection.py b/azext_iot/sdk/dps/service/models/twin_collection_py3.py similarity index 62% rename from azext_iot/sdk/dps/models/twin_collection.py rename to azext_iot/sdk/dps/service/models/twin_collection_py3.py index b3cf49987..232f7dedd 100644 --- a/azext_iot/sdk/dps/models/twin_collection.py +++ b/azext_iot/sdk/dps/service/models/twin_collection_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -9,18 +13,17 @@ class TwinCollection(Model): - """TwinCollection. + """Represents a collection of properties within a Twin. :param additional_properties: Unmatched properties from the message are deserialized this collection :type additional_properties: dict[str, object] - :param version: + :param version: Version of the TwinCollection :type version: long - :param count: + :param count: Number of properties in the TwinCollection :type count: int - :param metadata: - :type metadata: - ~microsoft.azure.management.provisioningservices.models.Metadata + :param metadata: Metadata for the TwinCollection + :type metadata: ~dps.models.Metadata """ _attribute_map = { @@ -30,8 +33,8 @@ class TwinCollection(Model): 'metadata': {'key': 'metadata', 'type': 'Metadata'}, } - def __init__(self, additional_properties=None, version=None, count=None, metadata=None): - super(TwinCollection, self).__init__() + def __init__(self, *, additional_properties=None, version: int=None, count: int=None, metadata=None, **kwargs) -> None: + super(TwinCollection, self).__init__(**kwargs) self.additional_properties = additional_properties self.version = version self.count = count diff --git a/azext_iot/sdk/dps/service/models/x509_attestation.py b/azext_iot/sdk/dps/service/models/x509_attestation.py new file mode 100644 index 000000000..2ffedb0cb --- /dev/null +++ b/azext_iot/sdk/dps/service/models/x509_attestation.py @@ -0,0 +1,36 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class X509Attestation(Model): + """Attestation via X509. + + :param client_certificates: + :type client_certificates: ~dps.models.X509Certificates + :param signing_certificates: + :type signing_certificates: ~dps.models.X509Certificates + :param ca_references: + :type ca_references: ~dps.models.X509CAReferences + """ + + _attribute_map = { + 'client_certificates': {'key': 'clientCertificates', 'type': 'X509Certificates'}, + 'signing_certificates': {'key': 'signingCertificates', 'type': 'X509Certificates'}, + 'ca_references': {'key': 'caReferences', 'type': 'X509CAReferences'}, + } + + def __init__(self, **kwargs): + super(X509Attestation, self).__init__(**kwargs) + self.client_certificates = kwargs.get('client_certificates', None) + self.signing_certificates = kwargs.get('signing_certificates', None) + self.ca_references = kwargs.get('ca_references', None) diff --git a/azext_iot/sdk/dps/models/x509_attestation.py b/azext_iot/sdk/dps/service/models/x509_attestation_py3.py similarity index 65% rename from azext_iot/sdk/dps/models/x509_attestation.py rename to azext_iot/sdk/dps/service/models/x509_attestation_py3.py index 56cd4d968..46184475c 100644 --- a/azext_iot/sdk/dps/models/x509_attestation.py +++ b/azext_iot/sdk/dps/service/models/x509_attestation_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -12,14 +16,11 @@ class X509Attestation(Model): """Attestation via X509. :param client_certificates: - :type client_certificates: - ~microsoft.azure.management.provisioningservices.models.X509Certificates + :type client_certificates: ~dps.models.X509Certificates :param signing_certificates: - :type signing_certificates: - ~microsoft.azure.management.provisioningservices.models.X509Certificates + :type signing_certificates: ~dps.models.X509Certificates :param ca_references: - :type ca_references: - ~microsoft.azure.management.provisioningservices.models.X509CAReferences + :type ca_references: ~dps.models.X509CAReferences """ _attribute_map = { @@ -28,8 +29,8 @@ class X509Attestation(Model): 'ca_references': {'key': 'caReferences', 'type': 'X509CAReferences'}, } - def __init__(self, client_certificates=None, signing_certificates=None, ca_references=None): - super(X509Attestation, self).__init__() + def __init__(self, *, client_certificates=None, signing_certificates=None, ca_references=None, **kwargs) -> None: + super(X509Attestation, self).__init__(**kwargs) self.client_certificates = client_certificates self.signing_certificates = signing_certificates self.ca_references = ca_references diff --git a/azext_iot/sdk/dps/service/models/x509_ca_references.py b/azext_iot/sdk/dps/service/models/x509_ca_references.py new file mode 100644 index 000000000..be2306bd8 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/x509_ca_references.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class X509CAReferences(Model): + """Primary and secondary CA references. + + :param primary: + :type primary: str + :param secondary: + :type secondary: str + """ + + _attribute_map = { + 'primary': {'key': 'primary', 'type': 'str'}, + 'secondary': {'key': 'secondary', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(X509CAReferences, self).__init__(**kwargs) + self.primary = kwargs.get('primary', None) + self.secondary = kwargs.get('secondary', None) diff --git a/azext_iot/sdk/dps/models/x509_ca_references.py b/azext_iot/sdk/dps/service/models/x509_ca_references_py3.py similarity index 71% rename from azext_iot/sdk/dps/models/x509_ca_references.py rename to azext_iot/sdk/dps/service/models/x509_ca_references_py3.py index b08341f52..4a85172ae 100644 --- a/azext_iot/sdk/dps/models/x509_ca_references.py +++ b/azext_iot/sdk/dps/service/models/x509_ca_references_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -22,7 +26,7 @@ class X509CAReferences(Model): 'secondary': {'key': 'secondary', 'type': 'str'}, } - def __init__(self, primary=None, secondary=None): - super(X509CAReferences, self).__init__() + def __init__(self, *, primary: str=None, secondary: str=None, **kwargs) -> None: + super(X509CAReferences, self).__init__(**kwargs) self.primary = primary self.secondary = secondary diff --git a/azext_iot/sdk/dps/service/models/x509_certificate_info.py b/azext_iot/sdk/dps/service/models/x509_certificate_info.py new file mode 100644 index 000000000..2eb783782 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/x509_certificate_info.py @@ -0,0 +1,69 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class X509CertificateInfo(Model): + """X509 certificate info. + + All required parameters must be populated in order to send to Azure. + + :param subject_name: Required. + :type subject_name: str + :param sha1_thumbprint: Required. + :type sha1_thumbprint: str + :param sha256_thumbprint: Required. + :type sha256_thumbprint: str + :param issuer_name: Required. + :type issuer_name: str + :param not_before_utc: Required. + :type not_before_utc: datetime + :param not_after_utc: Required. + :type not_after_utc: datetime + :param serial_number: Required. + :type serial_number: str + :param version: Required. + :type version: int + """ + + _validation = { + 'subject_name': {'required': True}, + 'sha1_thumbprint': {'required': True}, + 'sha256_thumbprint': {'required': True}, + 'issuer_name': {'required': True}, + 'not_before_utc': {'required': True}, + 'not_after_utc': {'required': True}, + 'serial_number': {'required': True}, + 'version': {'required': True}, + } + + _attribute_map = { + 'subject_name': {'key': 'subjectName', 'type': 'str'}, + 'sha1_thumbprint': {'key': 'sha1Thumbprint', 'type': 'str'}, + 'sha256_thumbprint': {'key': 'sha256Thumbprint', 'type': 'str'}, + 'issuer_name': {'key': 'issuerName', 'type': 'str'}, + 'not_before_utc': {'key': 'notBeforeUtc', 'type': 'iso-8601'}, + 'not_after_utc': {'key': 'notAfterUtc', 'type': 'iso-8601'}, + 'serial_number': {'key': 'serialNumber', 'type': 'str'}, + 'version': {'key': 'version', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(X509CertificateInfo, self).__init__(**kwargs) + self.subject_name = kwargs.get('subject_name', None) + self.sha1_thumbprint = kwargs.get('sha1_thumbprint', None) + self.sha256_thumbprint = kwargs.get('sha256_thumbprint', None) + self.issuer_name = kwargs.get('issuer_name', None) + self.not_before_utc = kwargs.get('not_before_utc', None) + self.not_after_utc = kwargs.get('not_after_utc', None) + self.serial_number = kwargs.get('serial_number', None) + self.version = kwargs.get('version', None) diff --git a/azext_iot/sdk/dps/models/x509_certificate_info.py b/azext_iot/sdk/dps/service/models/x509_certificate_info_py3.py similarity index 71% rename from azext_iot/sdk/dps/models/x509_certificate_info.py rename to azext_iot/sdk/dps/service/models/x509_certificate_info_py3.py index 1d0c92ab1..dc1ebb97f 100644 --- a/azext_iot/sdk/dps/models/x509_certificate_info.py +++ b/azext_iot/sdk/dps/service/models/x509_certificate_info_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -11,21 +15,23 @@ class X509CertificateInfo(Model): """X509 certificate info. - :param subject_name: + All required parameters must be populated in order to send to Azure. + + :param subject_name: Required. :type subject_name: str - :param sha1_thumbprint: + :param sha1_thumbprint: Required. :type sha1_thumbprint: str - :param sha256_thumbprint: + :param sha256_thumbprint: Required. :type sha256_thumbprint: str - :param issuer_name: + :param issuer_name: Required. :type issuer_name: str - :param not_before_utc: + :param not_before_utc: Required. :type not_before_utc: datetime - :param not_after_utc: + :param not_after_utc: Required. :type not_after_utc: datetime - :param serial_number: + :param serial_number: Required. :type serial_number: str - :param version: + :param version: Required. :type version: int """ @@ -51,8 +57,8 @@ class X509CertificateInfo(Model): 'version': {'key': 'version', 'type': 'int'}, } - def __init__(self, subject_name, sha1_thumbprint, sha256_thumbprint, issuer_name, not_before_utc, not_after_utc, serial_number, version): - super(X509CertificateInfo, self).__init__() + def __init__(self, *, subject_name: str, sha1_thumbprint: str, sha256_thumbprint: str, issuer_name: str, not_before_utc, not_after_utc, serial_number: str, version: int, **kwargs) -> None: + super(X509CertificateInfo, self).__init__(**kwargs) self.subject_name = subject_name self.sha1_thumbprint = sha1_thumbprint self.sha256_thumbprint = sha256_thumbprint diff --git a/azext_iot/sdk/dps/service/models/x509_certificate_with_info.py b/azext_iot/sdk/dps/service/models/x509_certificate_with_info.py new file mode 100644 index 000000000..36bc3d09e --- /dev/null +++ b/azext_iot/sdk/dps/service/models/x509_certificate_with_info.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class X509CertificateWithInfo(Model): + """Certificate and Certificate info. + + :param certificate: + :type certificate: str + :param info: + :type info: ~dps.models.X509CertificateInfo + """ + + _attribute_map = { + 'certificate': {'key': 'certificate', 'type': 'str'}, + 'info': {'key': 'info', 'type': 'X509CertificateInfo'}, + } + + def __init__(self, **kwargs): + super(X509CertificateWithInfo, self).__init__(**kwargs) + self.certificate = kwargs.get('certificate', None) + self.info = kwargs.get('info', None) diff --git a/azext_iot/sdk/dps/models/x509_certificate_with_info.py b/azext_iot/sdk/dps/service/models/x509_certificate_with_info_py3.py similarity index 67% rename from azext_iot/sdk/dps/models/x509_certificate_with_info.py rename to azext_iot/sdk/dps/service/models/x509_certificate_with_info_py3.py index 0704b6e4a..a54d66939 100644 --- a/azext_iot/sdk/dps/models/x509_certificate_with_info.py +++ b/azext_iot/sdk/dps/service/models/x509_certificate_with_info_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -14,8 +18,7 @@ class X509CertificateWithInfo(Model): :param certificate: :type certificate: str :param info: - :type info: - ~microsoft.azure.management.provisioningservices.models.X509CertificateInfo + :type info: ~dps.models.X509CertificateInfo """ _attribute_map = { @@ -23,7 +26,7 @@ class X509CertificateWithInfo(Model): 'info': {'key': 'info', 'type': 'X509CertificateInfo'}, } - def __init__(self, certificate=None, info=None): - super(X509CertificateWithInfo, self).__init__() + def __init__(self, *, certificate: str=None, info=None, **kwargs) -> None: + super(X509CertificateWithInfo, self).__init__(**kwargs) self.certificate = certificate self.info = info diff --git a/azext_iot/sdk/dps/service/models/x509_certificates.py b/azext_iot/sdk/dps/service/models/x509_certificates.py new file mode 100644 index 000000000..c549e0da2 --- /dev/null +++ b/azext_iot/sdk/dps/service/models/x509_certificates.py @@ -0,0 +1,32 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class X509Certificates(Model): + """Primary and secondary certificates. + + :param primary: + :type primary: ~dps.models.X509CertificateWithInfo + :param secondary: + :type secondary: ~dps.models.X509CertificateWithInfo + """ + + _attribute_map = { + 'primary': {'key': 'primary', 'type': 'X509CertificateWithInfo'}, + 'secondary': {'key': 'secondary', 'type': 'X509CertificateWithInfo'}, + } + + def __init__(self, **kwargs): + super(X509Certificates, self).__init__(**kwargs) + self.primary = kwargs.get('primary', None) + self.secondary = kwargs.get('secondary', None) diff --git a/azext_iot/sdk/dps/models/x509_certificates.py b/azext_iot/sdk/dps/service/models/x509_certificates_py3.py similarity index 64% rename from azext_iot/sdk/dps/models/x509_certificates.py rename to azext_iot/sdk/dps/service/models/x509_certificates_py3.py index 8845b060c..08d848954 100644 --- a/azext_iot/sdk/dps/models/x509_certificates.py +++ b/azext_iot/sdk/dps/service/models/x509_certificates_py3.py @@ -1,5 +1,9 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. @@ -12,11 +16,9 @@ class X509Certificates(Model): """Primary and secondary certificates. :param primary: - :type primary: - ~microsoft.azure.management.provisioningservices.models.X509CertificateWithInfo + :type primary: ~dps.models.X509CertificateWithInfo :param secondary: - :type secondary: - ~microsoft.azure.management.provisioningservices.models.X509CertificateWithInfo + :type secondary: ~dps.models.X509CertificateWithInfo """ _attribute_map = { @@ -24,7 +26,7 @@ class X509Certificates(Model): 'secondary': {'key': 'secondary', 'type': 'X509CertificateWithInfo'}, } - def __init__(self, primary=None, secondary=None): - super(X509Certificates, self).__init__() + def __init__(self, *, primary=None, secondary=None, **kwargs) -> None: + super(X509Certificates, self).__init__(**kwargs) self.primary = primary self.secondary = secondary diff --git a/azext_iot/sdk/dps/service/provisioning_service_client.py b/azext_iot/sdk/dps/service/provisioning_service_client.py new file mode 100644 index 000000000..a7a0fa314 --- /dev/null +++ b/azext_iot/sdk/dps/service/provisioning_service_client.py @@ -0,0 +1,1023 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.service_client import SDKClient +from msrest import Serializer, Deserializer +from msrestazure import AzureConfiguration +from .version import VERSION +from msrest.pipeline import ClientRawResponse +import uuid +from . import models +from ....constants import USER_AGENT + + +class ProvisioningServiceClientConfiguration(AzureConfiguration): + """Configuration for ProvisioningServiceClient + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if not base_url: + base_url = 'https://your-dps.azure-devices-provisioning.net' + + super(ProvisioningServiceClientConfiguration, self).__init__(base_url) + + self.add_user_agent('provisioningserviceclient/{}'.format(VERSION)) + self.add_user_agent(USER_AGENT) # @c-ryan-k - added CLI UA string + + self.credentials = credentials + + +class ProvisioningServiceClient(SDKClient): + """API for service operations with the Azure IoT Hub Device Provisioning Service + + :ivar config: Configuration for client. + :vartype config: ProvisioningServiceClientConfiguration + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param str base_url: Service URL + """ + + def __init__( + self, credentials, base_url=None): + + self.config = ProvisioningServiceClientConfiguration(credentials, base_url) + super(ProvisioningServiceClient, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2019-03-31' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + + def get_individual_enrollment( + self, id, custom_headers=None, raw=False, **operation_config): + """Get a device enrollment record. + + :param id: Registration ID. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: IndividualEnrollment or ClientRawResponse if raw=true + :rtype: ~dps.models.IndividualEnrollment or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.get_individual_enrollment.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('IndividualEnrollment', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_individual_enrollment.metadata = {'url': '/enrollments/{id}'} + + def create_or_update_individual_enrollment( + self, id, enrollment, if_match=None, custom_headers=None, raw=False, **operation_config): + """Create or update a device enrollment record. + + :param id: The registration ID is alphanumeric, lowercase, and may + contain hyphens. + :type id: str + :param enrollment: The device enrollment record. + :type enrollment: ~dps.models.IndividualEnrollment + :param if_match: The ETag of the enrollment record. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: IndividualEnrollment or ClientRawResponse if raw=true + :rtype: ~dps.models.IndividualEnrollment or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.create_or_update_individual_enrollment.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(enrollment, 'IndividualEnrollment') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('IndividualEnrollment', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create_or_update_individual_enrollment.metadata = {'url': '/enrollments/{id}'} + + def delete_individual_enrollment( + self, id, if_match=None, custom_headers=None, raw=False, **operation_config): + """Delete a device enrollment record. + + :param id: Registration ID. + :type id: str + :param if_match: The ETag of the enrollment record. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.delete_individual_enrollment.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete_individual_enrollment.metadata = {'url': '/enrollments/{id}'} + + def get_enrollment_group( + self, id, custom_headers=None, raw=False, **operation_config): + """Get a device enrollment group. + + :param id: Enrollment group ID. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: EnrollmentGroup or ClientRawResponse if raw=true + :rtype: ~dps.models.EnrollmentGroup or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.get_enrollment_group.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('EnrollmentGroup', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_enrollment_group.metadata = {'url': '/enrollmentGroups/{id}'} + + def create_or_update_enrollment_group( + self, id, enrollment_group, if_match=None, custom_headers=None, raw=False, **operation_config): + """Create or update a device enrollment group. + + :param id: Enrollment group ID. + :type id: str + :param enrollment_group: The device enrollment group. + :type enrollment_group: ~dps.models.EnrollmentGroup + :param if_match: The ETag of the enrollment record. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: EnrollmentGroup or ClientRawResponse if raw=true + :rtype: ~dps.models.EnrollmentGroup or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.create_or_update_enrollment_group.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(enrollment_group, 'EnrollmentGroup') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('EnrollmentGroup', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create_or_update_enrollment_group.metadata = {'url': '/enrollmentGroups/{id}'} + + def delete_enrollment_group( + self, id, if_match=None, custom_headers=None, raw=False, **operation_config): + """Delete a device enrollment group. + + :param id: Enrollment group ID. + :type id: str + :param if_match: The ETag of the enrollment group record. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.delete_enrollment_group.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete_enrollment_group.metadata = {'url': '/enrollmentGroups/{id}'} + + def get_device_registration_state( + self, id, custom_headers=None, raw=False, **operation_config): + """Gets the device registration state. + + :param id: Registration ID. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: DeviceRegistrationState or ClientRawResponse if raw=true + :rtype: ~dps.models.DeviceRegistrationState or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.get_device_registration_state.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('DeviceRegistrationState', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_device_registration_state.metadata = {'url': '/registrations/{id}'} + + def delete_device_registration_state( + self, id, if_match=None, custom_headers=None, raw=False, **operation_config): + """Deletes the device registration. + + :param id: Registration ID. + :type id: str + :param if_match: The ETag of the registration status record. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.delete_device_registration_state.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete_device_registration_state.metadata = {'url': '/registrations/{id}'} + + def query_individual_enrollments( + self, query, x_ms_max_item_count=None, x_ms_continuation=None, custom_headers=None, raw=False, **operation_config): + """Query the device enrollment records. + + :param query: The query. + :type query: str + :param x_ms_max_item_count: Page size + :type x_ms_max_item_count: int + :param x_ms_continuation: Continuation token + :type x_ms_continuation: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: list or ClientRawResponse if raw=true + :rtype: list[~dps.models.IndividualEnrollment] or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + query_specification = models.QuerySpecification(query=query) + + api_version = "2019-03-31" + + # Construct URL + url = self.query_individual_enrollments.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_max_item_count is not None: + header_parameters['x-ms-max-item-count'] = self._serialize.header("x_ms_max_item_count", x_ms_max_item_count, 'int') + if x_ms_continuation is not None: + header_parameters['x-ms-continuation'] = self._serialize.header("x_ms_continuation", x_ms_continuation, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(query_specification, 'QuerySpecification') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('[IndividualEnrollment]', response) + header_dict = { + 'x-ms-continuation': 'str', + 'x-ms-max-item-count': 'int', + 'x-ms-item-type': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + query_individual_enrollments.metadata = {'url': '/enrollments/query'} + + def get_individual_enrollment_attestation_mechanism( + self, id, custom_headers=None, raw=False, **operation_config): + """Get the attestation mechanism in the device enrollment record. + + :param id: Registration ID. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: AttestationMechanism or ClientRawResponse if raw=true + :rtype: ~dps.models.AttestationMechanism or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.get_individual_enrollment_attestation_mechanism.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('AttestationMechanism', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_individual_enrollment_attestation_mechanism.metadata = {'url': '/enrollments/{id}/attestationmechanism'} + + def run_bulk_individual_enrollment_operation( + self, enrollments, mode, custom_headers=None, raw=False, **operation_config): + """Bulk device enrollment operation with maximum of 10 enrollments. + + :param enrollments: Enrollment items + :type enrollments: list[~dps.models.IndividualEnrollment] + :param mode: Operation mode. Possible values include: 'create', + 'update', 'updateIfMatchETag', 'delete' + :type mode: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: BulkEnrollmentOperationResult or ClientRawResponse if + raw=true + :rtype: ~dps.models.BulkEnrollmentOperationResult or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + bulk_operation = models.BulkEnrollmentOperation(enrollments=enrollments, mode=mode) + + api_version = "2019-03-31" + + # Construct URL + url = self.run_bulk_individual_enrollment_operation.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(bulk_operation, 'BulkEnrollmentOperation') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('BulkEnrollmentOperationResult', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + run_bulk_individual_enrollment_operation.metadata = {'url': '/enrollments'} + + def query_enrollment_groups( + self, query, x_ms_max_item_count=None, x_ms_continuation=None, custom_headers=None, raw=False, **operation_config): + """Query the device enrollment groups. + + :param query: The query. + :type query: str + :param x_ms_max_item_count: Page size + :type x_ms_max_item_count: int + :param x_ms_continuation: Continuation token + :type x_ms_continuation: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: list or ClientRawResponse if raw=true + :rtype: list[~dps.models.EnrollmentGroup] or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + query_specification = models.QuerySpecification(query=query) + + api_version = "2019-03-31" + + # Construct URL + url = self.query_enrollment_groups.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_max_item_count is not None: + header_parameters['x-ms-max-item-count'] = self._serialize.header("x_ms_max_item_count", x_ms_max_item_count, 'int') + if x_ms_continuation is not None: + header_parameters['x-ms-continuation'] = self._serialize.header("x_ms_continuation", x_ms_continuation, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(query_specification, 'QuerySpecification') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('[EnrollmentGroup]', response) + header_dict = { + 'x-ms-continuation': 'str', + 'x-ms-max-item-count': 'int', + 'x-ms-item-type': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + query_enrollment_groups.metadata = {'url': '/enrollmentGroups/query'} + + def get_enrollment_group_attestation_mechanism( + self, id, custom_headers=None, raw=False, **operation_config): + """Get the attestation mechanism in the device enrollment group record. + + :param id: Enrollment group ID + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: AttestationMechanism or ClientRawResponse if raw=true + :rtype: ~dps.models.AttestationMechanism or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.get_enrollment_group_attestation_mechanism.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('AttestationMechanism', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_enrollment_group_attestation_mechanism.metadata = {'url': '/enrollmentGroups/{id}/attestationmechanism'} + + def run_bulk_enrollment_group_operation( + self, enrollment_groups, mode, custom_headers=None, raw=False, **operation_config): + """Bulk device enrollment group operation with maximum of 10 groups. + + :param enrollment_groups: Enrollment items + :type enrollment_groups: list[~dps.models.EnrollmentGroup] + :param mode: Operation mode. Possible values include: 'create', + 'update', 'updateIfMatchETag', 'delete' + :type mode: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: BulkEnrollmentGroupOperationResult or ClientRawResponse if + raw=true + :rtype: ~dps.models.BulkEnrollmentGroupOperationResult or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + bulk_operation = models.BulkEnrollmentGroupOperation(enrollment_groups=enrollment_groups, mode=mode) + + api_version = "2019-03-31" + + # Construct URL + url = self.run_bulk_enrollment_group_operation.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(bulk_operation, 'BulkEnrollmentGroupOperation') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('BulkEnrollmentGroupOperationResult', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + run_bulk_enrollment_group_operation.metadata = {'url': '/enrollmentGroups'} + + def query_device_registration_states( + self, id, x_ms_max_item_count=None, x_ms_continuation=None, custom_headers=None, raw=False, **operation_config): + """Gets the registration state of devices in this enrollmentGroup. + + :param id: Enrollment group ID. + :type id: str + :param x_ms_max_item_count: pageSize + :type x_ms_max_item_count: int + :param x_ms_continuation: continuation token + :type x_ms_continuation: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: list or ClientRawResponse if raw=true + :rtype: list[~dps.models.DeviceRegistrationState] or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ProvisioningServiceErrorDetailsException` + """ + api_version = "2019-03-31" + + # Construct URL + url = self.query_device_registration_states.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_max_item_count is not None: + header_parameters['x-ms-max-item-count'] = self._serialize.header("x_ms_max_item_count", x_ms_max_item_count, 'int') + if x_ms_continuation is not None: + header_parameters['x-ms-continuation'] = self._serialize.header("x_ms_continuation", x_ms_continuation, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ProvisioningServiceErrorDetailsException(self._deserialize, response) + + deserialized = None + header_dict = {} + + if response.status_code == 200: + deserialized = self._deserialize('[DeviceRegistrationState]', response) + header_dict = { + 'x-ms-continuation': 'str', + 'x-ms-max-item-count': 'int', + 'x-ms-item-type': 'str', + } + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + client_raw_response.add_headers(header_dict) + return client_raw_response + + return deserialized + query_device_registration_states.metadata = {'url': '/registrations/{id}/query'} diff --git a/azext_iot/sdk/dps/version.py b/azext_iot/sdk/dps/service/version.py similarity index 63% rename from azext_iot/sdk/dps/version.py rename to azext_iot/sdk/dps/service/version.py index e6c13ab40..a2a4832f8 100644 --- a/azext_iot/sdk/dps/version.py +++ b/azext_iot/sdk/dps/service/version.py @@ -1,9 +1,13 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# # Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is # regenerated. # -------------------------------------------------------------------------- -VERSION = "2018-09-01-preview" +VERSION = "2019-03-31" diff --git a/azext_iot/tests/test_iot_dps_int.py b/azext_iot/tests/test_iot_dps_int.py index b3eb92174..41cca7d11 100644 --- a/azext_iot/tests/test_iot_dps_int.py +++ b/azext_iot/tests/test_iot_dps_int.py @@ -8,333 +8,547 @@ from azure.cli.testsdk import LiveScenarioTest from azext_iot.common.shared import EntityStatusType, AttestationType, AllocationType from azext_iot.common.certops import create_self_signed_certificate +from azext_iot.common import embedded_cli +from azext_iot.iothub.providers.discovery import IotHubDiscovery +from .settings import Setting # Set these to the proper IoT Hub DPS, IoT Hub and Resource Group for Integration Tests. -dps = os.environ.get('azext_iot_testdps') -rg = os.environ.get('azext_iot_testrg') -hub = os.environ.get('azext_iot_testhub') +dps = os.environ.get("azext_iot_testdps") +rg = os.environ.get("azext_iot_testrg") +hub = os.environ.get("azext_iot_testhub") if not all([dps, rg, hub]): - raise ValueError('Set azext_iot_testhub, azext_iot_testdps ' - 'and azext_iot_testrg to run integration tests.') - -cert_name = 'test' -cert_path = cert_name + '-cert.pem' -test_endorsement_key = ('AToAAQALAAMAsgAgg3GXZ0SEs/gakMyNRqXXJP1S124GUgtk8qHaGzMUaaoABgCAAEMAEAgAAAAAAAEAibym9HQP9vxCGF5dVc1Q' - 'QsAGe021aUGJzNol1/gycBx3jFsTpwmWbISRwnFvflWd0w2Mc44FAAZNaJOAAxwZvG8GvyLlHh6fGKdh+mSBL4iLH2bZ4Ry22cB3' - 'CJVjXmdGoz9Y/j3/NwLndBxQC+baNvzvyVQZ4/A2YL7vzIIj2ik4y+ve9ir7U0GbNdnxskqK1KFIITVVtkTIYyyFTIR0BySjPrRI' - 'Dj7r7Mh5uF9HBppGKQCBoVSVV8dI91lNazmSdpGWyqCkO7iM4VvUMv2HT/ym53aYlUrau+Qq87Tu+uQipWYgRdF11KDfcpMHqqzB' - 'QQ1NpOJVhrsTrhyJzO7KNw==') - - -class IoTDpsTest(LiveScenarioTest): + raise ValueError( + "Set azext_iot_testhub, azext_iot_testdps " + "and azext_iot_testrg to run integration tests." + ) + +cert_name = "test" +cert_path = cert_name + "-cert.pem" +test_endorsement_key = ( + "AToAAQALAAMAsgAgg3GXZ0SEs/gakMyNRqXXJP1S124GUgtk8qHaGzMUaaoABgCAAEMAEAgAAAAAAAEAibym9HQP9vxCGF5dVc1Q" + "QsAGe021aUGJzNol1/gycBx3jFsTpwmWbISRwnFvflWd0w2Mc44FAAZNaJOAAxwZvG8GvyLlHh6fGKdh+mSBL4iLH2bZ4Ry22cB3" + "CJVjXmdGoz9Y/j3/NwLndBxQC+baNvzvyVQZ4/A2YL7vzIIj2ik4y+ve9ir7U0GbNdnxskqK1KFIITVVtkTIYyyFTIR0BySjPrRI" + "Dj7r7Mh5uF9HBppGKQCBoVSVV8dI91lNazmSdpGWyqCkO7iM4VvUMv2HT/ym53aYlUrau+Qq87Tu+uQipWYgRdF11KDfcpMHqqzB" + "QQ1NpOJVhrsTrhyJzO7KNw==" +) +provisioning_status = EntityStatusType.enabled.value +provisioning_status_new = EntityStatusType.disabled.value + + +def _cleanup_enrollments(self, dps, rg): + enrollments = self.cmd( + "iot dps enrollment list --dps-name {} -g {}".format(dps, rg) + ).get_output_in_json() + if len(enrollments) > 0: + enrollment_ids = list(map(lambda x: x["registrationId"], enrollments)) + for id in enrollment_ids: + self.cmd( + "iot dps enrollment delete --dps-name {} -g {} --enrollment-id {}".format( + dps, rg, id + ) + ) + + enrollment_groups = self.cmd( + "iot dps enrollment-group list --dps-name {} -g {}".format(dps, rg) + ).get_output_in_json() + if len(enrollment_groups) > 0: + enrollment_ids = list(map(lambda x: x["enrollmentGroupId"], enrollment_groups)) + for id in enrollment_ids: + self.cmd( + "iot dps enrollment-group delete --dps-name {} -g {} --enrollment-id {}".format( + dps, rg, id + ) + ) + + self.cmd( + "iot dps enrollment list --dps-name {} -g {}".format(dps, rg), + checks=self.is_empty(), + ) + self.cmd( + "iot dps enrollment-group list --dps-name {} -g {}".format(dps, rg), + checks=self.is_empty(), + ) + + +def _ensure_dps_hub_link(self, dps, rg, hub): + cli = embedded_cli.EmbeddedCLI() + hubs = cli.invoke( + "iot dps linked-hub list --dps-name {} -g {}".format(dps, rg) + ).as_json() + if not len(hubs) or not len( + list( + filter( + lambda linked_hub: linked_hub["name"] + == "{}.azure-devices.net".format(hub), + hubs, + ) + ) + ): + discovery = IotHubDiscovery(self.cmd_shell) + target_hub = discovery.get_target(hub, rg) + cli.invoke( + "iot dps linked-hub create --dps-name {} -g {} --connection-string {} --location {}".format( + dps, rg, target_hub.get("cs"), target_hub.get("location") + ) + ) + + +class TestDPSEnrollments(LiveScenarioTest): + def __init__(self, test_method): + super(TestDPSEnrollments, self).__init__(test_method) + self.cmd_shell = Setting() + setattr(self.cmd_shell, "cli_ctx", self.cli_ctx) - provisioning_status = EntityStatusType.enabled.value - provisioning_status_new = EntityStatusType.disabled.value + _ensure_dps_hub_link(self, dps, rg, hub) - def __init__(self, test_method): - super(IoTDpsTest, self).__init__(test_method) output_dir = os.getcwd() create_self_signed_certificate(cert_name, 200, output_dir, True) - self.kwargs['generic_dict'] = {'count': None, 'key': 'value', 'metadata': None, 'version': None} + self.kwargs["generic_dict"] = { + "count": None, + "key": "value", + "metadata": None, + "version": None, + } + + _cleanup_enrollments(self, dps, rg) def __del__(self): if os.path.exists(cert_path): os.remove(cert_path) def test_dps_compute_device_key(self): - device_key = self.cmd('az iot dps compute-device-key --key "{}" ' - '--registration-id myarbitrarydeviceId'.format(test_endorsement_key)).output + device_key = self.cmd( + 'az iot dps compute-device-key --key "{}" ' + "--registration-id myarbitrarydeviceId".format(test_endorsement_key) + ).output device_key = device_key.strip("\"'\n") assert device_key == "cT/EXZvsplPEpT//p98Pc6sKh8mY3kYgSxavHwMkl7w=" def test_dps_enrollment_tpm_lifecycle(self): - enrollment_id = self.create_random_name('enrollment-for-test', length=48) - endorsement_key = test_endorsement_key - device_id = self.create_random_name('device-id-for-test', length=48) + enrollment_id = self.create_random_name("enrollment-for-test", length=48) + device_id = self.create_random_name("device-id-for-test", length=48) attestation_type = AttestationType.tpm.value - hub_host_name = '{}.azure-devices.net'.format(hub) - - etag = self.cmd('iot dps enrollment create --enrollment-id {} --attestation-type {}' - ' -g {} --dps-name {} --endorsement-key {}' - ' --provisioning-status {} --device-id {} --initial-twin-tags {}' - ' --initial-twin-properties {} --allocation-policy {} --iot-hubs {}' - .format(enrollment_id, attestation_type, rg, dps, endorsement_key, - self.provisioning_status, device_id, - '"{generic_dict}"', '"{generic_dict}"', AllocationType.static.value, hub_host_name), - checks=[ - self.check('attestation.type', attestation_type), - self.check('registrationId', enrollment_id), - self.check('provisioningStatus', - self.provisioning_status), - self.check('deviceId', device_id), - self.check('allocationPolicy', AllocationType.static.value), - self.check('iotHubs', hub_host_name.split()), - self.check('initialTwin.tags', - self.kwargs['generic_dict']), - self.check('initialTwin.properties.desired', - self.kwargs['generic_dict']), - self.exists('reprovisionPolicy'), - self.check('reprovisionPolicy.migrateDeviceData', True), - self.check('reprovisionPolicy.updateHubAssignment', True) - ]).get_output_in_json()['etag'] - - self.cmd('iot dps enrollment list -g {} --dps-name {}'.format(rg, dps), checks=[ - self.check('length(@)', 1), - self.check('[0].registrationId', enrollment_id) - ]) - - self.cmd('iot dps enrollment show -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id), - checks=[self.check('registrationId', enrollment_id)]) - - self.cmd('iot dps enrollment update -g {} --dps-name {} --enrollment-id {}' - ' --provisioning-status {} --etag {}' - .format(rg, dps, enrollment_id, self.provisioning_status_new, etag), - checks=[ - self.check('attestation.type', attestation_type), - self.check('registrationId', enrollment_id), - self.check('provisioningStatus', - self.provisioning_status_new), - self.check('deviceId', device_id), - self.check('allocationPolicy', AllocationType.static.value), - self.check('iotHubs', hub_host_name.split()), - self.exists('initialTwin.tags'), - self.exists('initialTwin.properties.desired') - ]) - - self.cmd('iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id)) + hub_host_name = "{}.azure-devices.net".format(hub) + + enrollment = self.cmd( + "iot dps enrollment create --enrollment-id {} --attestation-type {}" + " -g {} --dps-name {} --endorsement-key {}" + " --provisioning-status {} --device-id {} --initial-twin-tags {}" + " --initial-twin-properties {} --allocation-policy {} --iot-hubs {}".format( + enrollment_id, + attestation_type, + rg, + dps, + test_endorsement_key, + provisioning_status, + device_id, + '"{generic_dict}"', + '"{generic_dict}"', + AllocationType.static.value, + hub_host_name, + ), + checks=[ + self.check("attestation.type", attestation_type), + self.check("registrationId", enrollment_id), + self.check("provisioningStatus", provisioning_status), + self.check("deviceId", device_id), + self.check("allocationPolicy", AllocationType.static.value), + self.check("iotHubs", hub_host_name.split()), + self.check("initialTwin.tags", self.kwargs["generic_dict"]), + self.check( + "initialTwin.properties.desired", self.kwargs["generic_dict"] + ), + self.exists("reprovisionPolicy"), + self.check("reprovisionPolicy.migrateDeviceData", True), + self.check("reprovisionPolicy.updateHubAssignment", True), + ], + ).get_output_in_json() + etag = enrollment["etag"] + + self.cmd( + "iot dps enrollment list -g {} --dps-name {}".format(rg, dps), + checks=[ + self.check("length(@)", 1), + self.check("[0].registrationId", enrollment_id), + ], + ) + + self.cmd( + "iot dps enrollment show -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ), + checks=[self.check("registrationId", enrollment_id)], + ) + + self.cmd( + "iot dps enrollment show -g {} --dps-name {} --enrollment-id {} --show-keys".format( + rg, dps, enrollment_id + ), + checks=[ + self.check("registrationId", enrollment_id), + self.check("attestation.type", attestation_type), + self.exists("attestation.{}".format(attestation_type)), + ], + ) + + self.cmd( + "iot dps enrollment update -g {} --dps-name {} --enrollment-id {}" + " --provisioning-status {} --etag {}".format( + rg, dps, enrollment_id, provisioning_status_new, etag + ), + checks=[ + self.check("attestation.type", attestation_type), + self.check("registrationId", enrollment_id), + self.check("provisioningStatus", provisioning_status_new), + self.check("deviceId", device_id), + self.check("allocationPolicy", AllocationType.static.value), + self.check("iotHubs", hub_host_name.split()), + self.exists("initialTwin.tags"), + self.exists("initialTwin.properties.desired"), + ], + ) + + self.cmd( + "iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ) + ) def test_dps_enrollment_x509_lifecycle(self): - enrollment_id = self.create_random_name('enrollment-for-test', length=48) + enrollment_id = self.create_random_name("enrollment-for-test", length=48) attestation_type = AttestationType.x509.value - device_id = self.create_random_name('device-id-for-test', length=48) - hub_host_name = '{}.azure-devices.net'.format(hub) - - etag = self.cmd('iot dps enrollment create --enrollment-id {} --attestation-type {}' - ' -g {} --dps-name {} --cp {} --scp {}' - ' --provisioning-status {} --device-id {}' - ' --initial-twin-tags {} --initial-twin-properties {}' - ' --allocation-policy {} --iot-hubs {}' - .format(enrollment_id, attestation_type, rg, dps, cert_path, - cert_path, self.provisioning_status, device_id, - '"{generic_dict}"', '"{generic_dict}"', - AllocationType.hashed.value, - hub_host_name), - checks=[ - self.check('attestation.type', attestation_type), - self.check('registrationId', enrollment_id), - self.check('provisioningStatus', - self.provisioning_status), - self.check('deviceId', device_id), - self.check('allocationPolicy', AllocationType.hashed.value), - self.check('iotHubs', hub_host_name.split()), - self.check('initialTwin.tags', - self.kwargs['generic_dict']), - self.check('initialTwin.properties.desired', - self.kwargs['generic_dict']), - self.exists('reprovisionPolicy'), - self.check('reprovisionPolicy.migrateDeviceData', True), - self.check('reprovisionPolicy.updateHubAssignment', True) - ]).get_output_in_json()['etag'] - - self.cmd('iot dps enrollment list -g {} --dps-name {}'.format(rg, dps), - checks=[ - self.check('length(@)', 1), - self.check('[0].registrationId', enrollment_id)]) - - self.cmd('iot dps enrollment show -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id), - checks=[self.check('registrationId', enrollment_id)]) - - self.cmd('iot dps enrollment update -g {} --dps-name {} --enrollment-id {}' - ' --provisioning-status {} --etag {} --rc' - .format(rg, dps, enrollment_id, self.provisioning_status_new, etag), - checks=[ - self.check('attestation.type', attestation_type), - self.check('registrationId', enrollment_id), - self.check('provisioningStatus', - self.provisioning_status_new), - self.check('deviceId', device_id), - self.check('allocationPolicy', AllocationType.hashed.value), - self.check('iotHubs', hub_host_name.split()), - self.exists('initialTwin.tags'), - self.exists('initialTwin.properties.desired'), - self.check( - 'attestation.type.x509.clientCertificates.primary', None) - ]) - - self.cmd('iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id)) + device_id = self.create_random_name("device-id-for-test", length=48) + hub_host_name = "{}.azure-devices.net".format(hub) + + etag = self.cmd( + "iot dps enrollment create --enrollment-id {} --attestation-type {}" + " -g {} --dps-name {} --cp {} --scp {}" + " --provisioning-status {} --device-id {}" + " --initial-twin-tags {} --initial-twin-properties {}" + " --allocation-policy {} --iot-hubs {}".format( + enrollment_id, + attestation_type, + rg, + dps, + cert_path, + cert_path, + provisioning_status, + device_id, + '"{generic_dict}"', + '"{generic_dict}"', + AllocationType.hashed.value, + hub_host_name, + ), + checks=[ + self.check("attestation.type", attestation_type), + self.check("registrationId", enrollment_id), + self.check("provisioningStatus", provisioning_status), + self.check("deviceId", device_id), + self.check("allocationPolicy", AllocationType.hashed.value), + self.check("iotHubs", hub_host_name.split()), + self.check("initialTwin.tags", self.kwargs["generic_dict"]), + self.check( + "initialTwin.properties.desired", self.kwargs["generic_dict"] + ), + self.exists("reprovisionPolicy"), + self.check("reprovisionPolicy.migrateDeviceData", True), + self.check("reprovisionPolicy.updateHubAssignment", True), + ], + ).get_output_in_json()["etag"] + + self.cmd( + "iot dps enrollment list -g {} --dps-name {}".format(rg, dps), + checks=[ + self.check("length(@)", 1), + self.check("[0].registrationId", enrollment_id), + ], + ) + + self.cmd( + "iot dps enrollment show -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ), + checks=[self.check("registrationId", enrollment_id)], + ) + + self.cmd( + "iot dps enrollment update -g {} --dps-name {} --enrollment-id {}" + " --provisioning-status {} --etag {} --rc".format( + rg, dps, enrollment_id, provisioning_status_new, etag + ), + checks=[ + self.check("attestation.type", attestation_type), + self.check("registrationId", enrollment_id), + self.check("provisioningStatus", provisioning_status_new), + self.check("deviceId", device_id), + self.check("allocationPolicy", AllocationType.hashed.value), + self.check("iotHubs", hub_host_name.split()), + self.exists("initialTwin.tags"), + self.exists("initialTwin.properties.desired"), + self.check("attestation.type.x509.clientCertificates.primary", None), + ], + ) + + self.cmd( + "iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ) + ) def test_dps_enrollment_symmetrickey_lifecycle(self): - enrollment_id = self.create_random_name('enrollment-for-test', length=48) - enrollment_id2 = self.create_random_name('enrollment-for-test', length=48) + enrollment_id = self.create_random_name("enrollment-for-test", length=48) + enrollment_id2 = self.create_random_name("enrollment-for-test", length=48) attestation_type = AttestationType.symmetricKey.value - primary_key = 'x3XNu1HeSw93rmtDXduRUZjhqdGbcqR/zloWYiyPUzw=' - secondary_key = 'PahMnOSBblv9CRn5B765iK35jTvnjDUjYP9hKBZa4Ug=' - device_id = self.create_random_name('device-id-for-test', length=48) - reprovisionPolicy_reprovisionandresetdata = 'reprovisionandresetdata' - hub_host_name = '{}.azure-devices.net'.format(hub) - webhook_url = 'https://www.test.test' - api_version = '2019-03-31' - - etag = self.cmd('iot dps enrollment create --enrollment-id {} --attestation-type {}' - ' -g {} --dps-name {} --pk {} --sk {}' - ' --provisioning-status {} --device-id {}' - ' --initial-twin-tags {} --initial-twin-properties {}' - ' --allocation-policy {} --rp {} --iot-hubs {} --edge-enabled' - .format(enrollment_id, attestation_type, rg, dps, primary_key, - secondary_key, self.provisioning_status, device_id, - '"{generic_dict}"', '"{generic_dict}"', - AllocationType.geolatency.value, - reprovisionPolicy_reprovisionandresetdata, - hub_host_name), - checks=[ - self.check('attestation.type', attestation_type), - self.check('registrationId', enrollment_id), - self.check('provisioningStatus', - self.provisioning_status), - self.check('deviceId', device_id), - self.check('allocationPolicy', 'geoLatency'), - self.check('iotHubs', hub_host_name.split()), - self.check('initialTwin.tags', - self.kwargs['generic_dict']), - self.check('initialTwin.properties.desired', - self.kwargs['generic_dict']), - self.exists('reprovisionPolicy'), - self.check('reprovisionPolicy.migrateDeviceData', False), - self.check('reprovisionPolicy.updateHubAssignment', True), - self.check('capabilities.iotEdge', True) - ]).get_output_in_json()['etag'] - - self.cmd('iot dps enrollment list -g {} --dps-name {}'.format(rg, dps), - checks=[ - self.check('length(@)', 1), - self.check('[0].registrationId', enrollment_id)]) - - self.cmd('iot dps enrollment show -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id), - checks=[self.check('registrationId', enrollment_id)]) - - self.cmd('iot dps enrollment update -g {} --dps-name {} --enrollment-id {}' - ' --provisioning-status {} --etag {} --edge-enabled False' - ' --allocation-policy {} --webhook-url {} --api-version {}' - .format(rg, dps, enrollment_id, self.provisioning_status_new, etag, - AllocationType.custom.value, webhook_url, api_version), - checks=[ - self.check('attestation.type', attestation_type), - self.check('registrationId', enrollment_id), - self.check('provisioningStatus', - self.provisioning_status_new), - self.check('deviceId', device_id), - self.check('allocationPolicy', "custom"), - self.check('customAllocationDefinition.webhookUrl', webhook_url), - self.check('customAllocationDefinition.apiVersion', api_version), - # self.check('iotHubs', hub_host_name.split()), TODO - self.exists('initialTwin.tags'), - self.exists('initialTwin.properties.desired'), - self.check('attestation.symmetricKey.primaryKey', primary_key), - self.check('capabilities.iotEdge', False) - ]) - - self.cmd('iot dps enrollment create --enrollment-id {} --attestation-type {}' - ' -g {} --dps-name {} --allocation-policy {} --webhook-url {} --api-version {}' - .format(enrollment_id2, attestation_type, rg, dps, AllocationType.custom.value, - webhook_url, api_version), - checks=[ - self.check('attestation.type', attestation_type), - self.check('registrationId', enrollment_id2), - self.check('allocationPolicy', 'custom'), - self.check('customAllocationDefinition.webhookUrl', webhook_url), - self.check('customAllocationDefinition.apiVersion', api_version), - ]) - - self.cmd('iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id)) - self.cmd('iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id2)) + primary_key = "x3XNu1HeSw93rmtDXduRUZjhqdGbcqR/zloWYiyPUzw=" + secondary_key = "PahMnOSBblv9CRn5B765iK35jTvnjDUjYP9hKBZa4Ug=" + device_id = self.create_random_name("device-id-for-test", length=48) + reprovisionPolicy_reprovisionandresetdata = "reprovisionandresetdata" + hub_host_name = "{}.azure-devices.net".format(hub) + webhook_url = "https://www.test.test" + api_version = "2019-03-31" + + etag = self.cmd( + "iot dps enrollment create --enrollment-id {} --attestation-type {}" + " -g {} --dps-name {} --pk {} --sk {}" + " --provisioning-status {} --device-id {}" + " --initial-twin-tags {} --initial-twin-properties {}" + " --allocation-policy {} --rp {} --iot-hubs {} --edge-enabled".format( + enrollment_id, + attestation_type, + rg, + dps, + primary_key, + secondary_key, + provisioning_status, + device_id, + '"{generic_dict}"', + '"{generic_dict}"', + AllocationType.geolatency.value, + reprovisionPolicy_reprovisionandresetdata, + hub_host_name, + ), + checks=[ + self.check("attestation.type", attestation_type), + self.check("registrationId", enrollment_id), + self.check("provisioningStatus", provisioning_status), + self.check("deviceId", device_id), + self.check("allocationPolicy", "geoLatency"), + # self.check("iotHubs", hub_host_name.split()), + self.check("initialTwin.tags", self.kwargs["generic_dict"]), + self.check( + "initialTwin.properties.desired", self.kwargs["generic_dict"] + ), + self.exists("reprovisionPolicy"), + self.check("reprovisionPolicy.migrateDeviceData", False), + self.check("reprovisionPolicy.updateHubAssignment", True), + self.check("capabilities.iotEdge", True), + ], + ).get_output_in_json()["etag"] + + self.cmd( + "iot dps enrollment list -g {} --dps-name {}".format(rg, dps), + checks=[ + self.check("length(@)", 1), + self.check("[0].registrationId", enrollment_id), + ], + ) + + self.cmd( + "iot dps enrollment show -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ), + checks=[self.check("registrationId", enrollment_id)], + ) + + self.cmd( + "iot dps enrollment update -g {} --dps-name {} --enrollment-id {}" + " --provisioning-status {} --etag {} --edge-enabled False" + " --allocation-policy {} --webhook-url {} --api-version {}".format( + rg, + dps, + enrollment_id, + provisioning_status_new, + etag, + AllocationType.custom.value, + webhook_url, + api_version, + ), + checks=[ + self.check("attestation.type", attestation_type), + self.check("registrationId", enrollment_id), + self.check("provisioningStatus", provisioning_status_new), + self.check("deviceId", device_id), + self.check("allocationPolicy", "custom"), + self.check("customAllocationDefinition.webhookUrl", webhook_url), + self.check("customAllocationDefinition.apiVersion", api_version), + # self.check("iotHubs", hub_host_name.split()), + self.exists("initialTwin.tags"), + self.exists("initialTwin.properties.desired"), + self.check("attestation.symmetricKey.primaryKey", primary_key), + self.check("capabilities.iotEdge", False), + ], + ) + + self.cmd( + "iot dps enrollment create --enrollment-id {} --attestation-type {}" + " -g {} --dps-name {} --allocation-policy {} --webhook-url {} --api-version {}".format( + enrollment_id2, + attestation_type, + rg, + dps, + AllocationType.custom.value, + webhook_url, + api_version, + ), + checks=[ + self.check("attestation.type", attestation_type), + self.check("registrationId", enrollment_id2), + self.check("allocationPolicy", "custom"), + self.check("customAllocationDefinition.webhookUrl", webhook_url), + self.check("customAllocationDefinition.apiVersion", api_version), + ], + ) + + self.cmd( + "iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ) + ) + self.cmd( + "iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id2 + ) + ) def test_dps_enrollment_group_lifecycle(self): - enrollment_id = self.create_random_name('enrollment-for-test', length=48) - reprovisionPolicy_never = 'never' - hub_host_name = '{}.azure-devices.net'.format(hub) - webhook_url = 'https://www.test.test' - api_version = '2019-03-31' - etag = self.cmd('iot dps enrollment-group create --enrollment-id {} -g {} --dps-name {}' - ' --cp {} --scp {} --provisioning-status {} --allocation-policy {}' - ' --iot-hubs {} --edge-enabled' - .format(enrollment_id, rg, dps, cert_path, cert_path, - self.provisioning_status, "geoLatency", - hub_host_name), - checks=[ - self.check('enrollmentGroupId', enrollment_id), - self.check('provisioningStatus', - self.provisioning_status), - self.exists('reprovisionPolicy'), - self.check('allocationPolicy', "geoLatency"), - self.check('iotHubs', hub_host_name.split()), - self.check('reprovisionPolicy.migrateDeviceData', True), - self.check('reprovisionPolicy.updateHubAssignment', True), - self.check('capabilities.iotEdge', True) - ]).get_output_in_json()['etag'] - - self.cmd('iot dps enrollment-group list -g {} --dps-name {}'.format(rg, dps), checks=[ - self.check('length(@)', 1), - self.check('[0].enrollmentGroupId', enrollment_id) - ]) - - self.cmd('iot dps enrollment-group show -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id), - checks=[self.check('enrollmentGroupId', enrollment_id)]) - - etag = self.cmd('iot dps enrollment-group update -g {} --dps-name {} --enrollment-id {}' - ' --provisioning-status {} --rsc --etag {} --rp {} --allocation-policy {}' - ' --edge-enabled False --scp {}' - .format(rg, dps, enrollment_id, self.provisioning_status_new, etag, - reprovisionPolicy_never, AllocationType.hashed.value, - cert_path), - checks=[ - self.check('attestation.type', AttestationType.x509.value), - self.check('enrollmentGroupId', enrollment_id), - self.check('provisioningStatus', self.provisioning_status_new), - self.check('attestation.type.x509.clientCertificates.secondary', None), - self.exists('reprovisionPolicy'), - self.check('allocationPolicy', AllocationType.hashed.value), - self.check('reprovisionPolicy.migrateDeviceData', False), - self.check('reprovisionPolicy.updateHubAssignment', False), - self.check('capabilities.iotEdge', False) - ]).get_output_in_json()['etag'] - - self.cmd('iot dps registration list -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id), - checks=[self.check('length(@)', 0)]) - - cert_name = self.create_random_name('certificate-for-test', length=48) - cert_etag = self.cmd('iot dps certificate create -g {} --dps-name {} --name {} --p {}' - .format(rg, dps, cert_name, cert_path), - checks=[self.check('name', cert_name)]).get_output_in_json()['etag'] - - self.cmd('iot dps enrollment-group update -g {} --dps-name {} --enrollment-id {}' - ' --cn {} --etag {} --allocation-policy {} --webhook-url {} --api-version {}' - .format(rg, dps, enrollment_id, cert_name, etag, - AllocationType.custom.value, webhook_url, api_version), - checks=[ - self.check('attestation.type', - AttestationType.x509.value), - self.check('enrollmentGroupId', enrollment_id), - self.check('allocationPolicy', "custom"), - self.check('customAllocationDefinition.webhookUrl', webhook_url), - self.check('customAllocationDefinition.apiVersion', api_version), - self.check( - 'attestation.x509.caReferences.primary', cert_name), - self.check( - 'attestation.x509.caReferences.secondary', None) - ]) - - self.cmd('iot dps enrollment-group delete -g {} --dps-name {} --enrollment-id {}' - .format(rg, dps, enrollment_id)) - - self.cmd('iot dps certificate delete -g {} --dps-name {} --name {} --etag {}' - .format(rg, dps, cert_name, cert_etag)) + enrollment_id = self.create_random_name("enrollment-for-test", length=48) + reprovisionPolicy_never = "never" + hub_host_name = "{}.azure-devices.net".format(hub) + webhook_url = "https://www.test.test" + api_version = "2019-03-31" + etag = self.cmd( + "iot dps enrollment-group create --enrollment-id {} -g {} --dps-name {}" + " --cp {} --scp {} --provisioning-status {} --allocation-policy {}" + " --iot-hubs {} --edge-enabled".format( + enrollment_id, + rg, + dps, + cert_path, + cert_path, + provisioning_status, + "geoLatency", + hub_host_name, + ), + checks=[ + self.check("enrollmentGroupId", enrollment_id), + self.check("provisioningStatus", provisioning_status), + self.exists("reprovisionPolicy"), + self.check("allocationPolicy", "geoLatency"), + self.check("iotHubs", hub_host_name.split()), + self.check("reprovisionPolicy.migrateDeviceData", True), + self.check("reprovisionPolicy.updateHubAssignment", True), + self.check("capabilities.iotEdge", True), + ], + ).get_output_in_json()["etag"] + + self.cmd( + "iot dps enrollment-group list -g {} --dps-name {}".format(rg, dps), + checks=[ + self.check("length(@)", 1), + self.check("[0].enrollmentGroupId", enrollment_id), + ], + ) + + self.cmd( + "iot dps enrollment-group show -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ), + checks=[self.check("enrollmentGroupId", enrollment_id)], + ) + + self.cmd( + "iot dps enrollment-group show -g {} --dps-name {} --enrollment-id {} --show-keys".format( + rg, dps, enrollment_id + ), + checks=[ + self.check("enrollmentGroupId", enrollment_id), + self.exists("attestation.x509"), + ], + ) + + etag = self.cmd( + "iot dps enrollment-group update -g {} --dps-name {} --enrollment-id {}" + " --provisioning-status {} --rsc --etag {} --rp {} --allocation-policy {}" + " --edge-enabled False --scp {}".format( + rg, + dps, + enrollment_id, + provisioning_status_new, + etag, + reprovisionPolicy_never, + AllocationType.hashed.value, + cert_path, + ), + checks=[ + self.check("attestation.type", AttestationType.x509.value), + self.check("enrollmentGroupId", enrollment_id), + self.check("provisioningStatus", provisioning_status_new), + self.check("attestation.type.x509.clientCertificates.secondary", None), + self.exists("reprovisionPolicy"), + self.check("allocationPolicy", AllocationType.hashed.value), + self.check("reprovisionPolicy.migrateDeviceData", False), + self.check("reprovisionPolicy.updateHubAssignment", False), + self.check("capabilities.iotEdge", False), + ], + ).get_output_in_json()["etag"] + + self.cmd( + "iot dps registration list -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ), + checks=[self.check("length(@)", 0)], + ) + + cert_name = self.create_random_name("certificate-for-test", length=48) + cert_etag = self.cmd( + "iot dps certificate create -g {} --dps-name {} --name {} --p {}".format( + rg, dps, cert_name, cert_path + ), + checks=[self.check("name", cert_name)], + ).get_output_in_json()["etag"] + + self.cmd( + "iot dps enrollment-group update -g {} --dps-name {} --enrollment-id {}" + " --cn {} --etag {} --allocation-policy {} --webhook-url {} --api-version {}".format( + rg, + dps, + enrollment_id, + cert_name, + etag, + AllocationType.custom.value, + webhook_url, + api_version, + ), + checks=[ + self.check("attestation.type", AttestationType.x509.value), + self.check("enrollmentGroupId", enrollment_id), + self.check("allocationPolicy", "custom"), + self.check("customAllocationDefinition.webhookUrl", webhook_url), + self.check("customAllocationDefinition.apiVersion", api_version), + self.check("attestation.x509.caReferences.primary", cert_name), + self.check("attestation.x509.caReferences.secondary", None), + ], + ) + + self.cmd( + "iot dps enrollment-group delete -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ) + ) + + self.cmd( + "iot dps certificate delete -g {} --dps-name {} --name {} --etag {}".format( + rg, dps, cert_name, cert_etag + ) + ) diff --git a/azext_iot/tests/test_iot_dps_unit.py b/azext_iot/tests/test_iot_dps_unit.py index 8476df211..bfe308921 100644 --- a/azext_iot/tests/test_iot_dps_unit.py +++ b/azext_iot/tests/test_iot_dps_unit.py @@ -13,10 +13,10 @@ import pytest import json +import responses from azext_iot.operations import dps as subject from knack.util import CLIError from azext_iot.common.sas_token_auth import SasTokenAuthentication -from .conftest import build_mock_response enrollment_id = 'myenrollment' resource_group = 'myrg' @@ -31,6 +31,13 @@ mock_target['policy'] = 'provisioningserviceowner' mock_target['subscription'] = "5952cff8-bcd1-4235-9554-af2c0348bf23" +mock_symmetric_key_attestation = { + "type": "symmetricKey", + "symmetricKey": { + "primaryKey": "primary_key", + "secondaryKey": "secondary_key" + }, +} # Patch Paths # path_service_client = 'msrest.service_client.ServiceClient.send' @@ -40,8 +47,8 @@ @pytest.fixture() def fixture_gdcs(mocker): - ghcs = mocker.patch(path_gdcs) - ghcs.return_value = mock_target + gdcs = mocker.patch(path_gdcs) + gdcs.return_value = mock_target @pytest.fixture() @@ -53,13 +60,6 @@ def fixture_sas(mocker): sas.return_value = r -@pytest.fixture(params=[400, 401, 500]) -def serviceclient_generic_error(mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {'error': 'something failed'}) - return service_client - - def generate_enrollment_create_req(attestation_type=None, endorsement_key=None, certificate_path=None, secondary_certificate_path=None, device_Id=None, iot_hub_host_name=None, @@ -91,11 +91,29 @@ def generate_enrollment_create_req(attestation_type=None, endorsement_key=None, class TestEnrollmentCreate(): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client + @pytest.fixture() + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas): + mocked_response.add( + method=responses.PUT, + url="https://{}/enrollments/{}".format(mock_target['entity'], enrollment_id), + body='{}', + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response + + @pytest.fixture(params=[400, 401, 500]) + def serviceclient_generic_error(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.PUT, + url="https://{}/enrollments/{}".format(mock_target['entity'], enrollment_id), + body='{}', + status=request.param, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response @pytest.mark.parametrize("req", [ (generate_enrollment_create_req(attestation_type='tpm', @@ -165,8 +183,8 @@ def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): secondary_key='secondarykey', edge_enabled=True)), ]) - def test_enrollment_create(self, serviceclient, req): - subject.iot_dps_device_enrollment_create(None, + def test_enrollment_create(self, serviceclient, fixture_cmd, req): + subject.iot_dps_device_enrollment_create(fixture_cmd, req['enrollment_id'], req['attestation_type'], req['dps_name'], req['rg'], @@ -186,12 +204,12 @@ def test_enrollment_create(self, serviceclient, req): req['edge_enabled'], req['webhook_url'], req['api_version']) - args = serviceclient.call_args - url = args[0][0].url + request = serviceclient.calls[0].request + url = request.url assert "{}/enrollments/{}?".format(mock_target['entity'], enrollment_id) in url - assert args[0][0].method == 'PUT' + assert request.method == 'PUT' - body = args[0][2] + body = json.loads(request.body) assert body['registrationId'] == req['enrollment_id'] if req['attestation_type'] == 'tpm': assert body['attestation']['type'] == req['attestation_type'] @@ -258,9 +276,9 @@ def test_enrollment_create(self, serviceclient, req): (generate_enrollment_create_req(allocation_policy='static', iot_hub_host_name='hubname')), (generate_enrollment_create_req(iot_hubs='hub1 hub2')) ]) - def test_enrollment_create_invalid_args(self, serviceclient, req): + def test_enrollment_create_invalid_args(self, fixture_gdcs, fixture_cmd, req): with pytest.raises(CLIError): - subject.iot_dps_device_enrollment_create(None, req['enrollment_id'], + subject.iot_dps_device_enrollment_create(fixture_cmd, req['enrollment_id'], req['attestation_type'], req['dps_name'], req['rg'], req['endorsement_key'], @@ -349,15 +367,28 @@ def generate_enrollment_update_req(certificate_path=None, iot_hub_host_name=None class TestEnrollmentUpdate(): - @pytest.fixture(params=[(200, generate_enrollment_show(), 200)]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - test_side_effect = [ - build_mock_response(mocker, request.param[0], request.param[1]), - build_mock_response(mocker, request.param[2]) - ] - service_client.side_effect = test_side_effect - return service_client + @pytest.fixture() + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + # Initial GET + mocked_response.add( + method=responses.GET, + url="https://{}/enrollments/{}".format(mock_target['entity'], enrollment_id), + body=json.dumps(generate_enrollment_show()), + status=200, + content_type="application/json", + match_querystring=False, + ) + + # Update PUT + mocked_response.add( + method=responses.PUT, + url="https://{}/enrollments/{}".format(mock_target['entity'], enrollment_id), + body=json.dumps(generate_enrollment_show()), + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response @pytest.mark.parametrize("req", [ (generate_enrollment_update_req(etag=etag, secondary_certificate_path='someOtherCertPath')), @@ -404,14 +435,17 @@ def test_enrollment_update(self, serviceclient, req): req['edge_enabled'], req['webhook_url'], req['api_version']) - # Index 1 is the update args - args = serviceclient.call_args_list[1] - url = args[0][0].url + get_request = serviceclient.calls[0].request + assert get_request.method == 'GET' + assert "{}/enrollments/{}?".format(mock_target['entity'], enrollment_id) in get_request.url + + update_request = serviceclient.calls[1].request + url = update_request.url assert "{}/enrollments/{}?".format(mock_target['entity'], enrollment_id) in url - assert args[0][0].method == 'PUT' + assert update_request.method == 'PUT' - body = args[0][2] + body = json.loads(update_request.body) if not req['certificate_path']: if req['remove_certificate_path']: assert body['attestation']['x509']['clientCertificates'].get('primary') is None @@ -454,22 +488,73 @@ def test_enrollment_update(self, serviceclient, req): class TestEnrollmentShow(): @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, generate_enrollment_show()) - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.GET, + url="https://{}/enrollments/{}".format(mock_target['entity'], enrollment_id), + body=json.dumps(generate_enrollment_show()), + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.fixture() + def serviceclient_attestation(self, mocked_response, fixture_gdcs, fixture_sas): + mocked_response.add( + method=responses.GET, + url="https://{}/enrollments/{}".format(mock_target['entity'], enrollment_id), + body=json.dumps(generate_enrollment_show(attestation=mock_symmetric_key_attestation)), + status=200, + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.POST, + url="https://{}/enrollments/{}/attestationmechanism".format(mock_target['entity'], enrollment_id), + body=json.dumps(mock_symmetric_key_attestation), + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response def test_enrollment_show(self, serviceclient): result = subject.iot_dps_device_enrollment_get(None, enrollment_id, mock_target['entity'], resource_group) - assert result.registration_id == enrollment_id - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method + assert result['registrationId'] == enrollment_id + + request = serviceclient.calls[0].request + url = request.url + method = request.method + + assert "{}/enrollments/{}?".format(mock_target['entity'], enrollment_id) in url + assert method == 'GET' + + def test_enrollment_show_with_keys(self, serviceclient_attestation): + result = subject.iot_dps_device_enrollment_get(None, enrollment_id, + mock_target['entity'], resource_group, show_keys=True) + + assert result['registrationId'] == enrollment_id + assert result['attestation'] + + request = serviceclient_attestation.calls[0].request + url = request.url + method = request.method + assert "{}/enrollments/{}?".format(mock_target['entity'], enrollment_id) in url assert method == 'GET' + request = serviceclient_attestation.calls[1].request + url = request.url + method = request.method + + assert "{}/enrollments/{}/attestationmechanism?".format(mock_target['entity'], enrollment_id) in url + assert method == 'POST' + def test_enrollment_show_error(self, serviceclient_generic_error): with pytest.raises(CLIError): subject.iot_dps_device_enrollment_get(None, enrollment_id, @@ -478,18 +563,24 @@ def test_enrollment_show_error(self, serviceclient_generic_error): class TestEnrollmentList(): @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, [generate_enrollment_show()]) - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.POST, + url="https://{}/enrollments/query?".format(mock_target['entity']), + body=json.dumps([generate_enrollment_show()]), + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response @pytest.mark.parametrize("top", [3, None]) def test_enrollment_list(self, serviceclient, top): result = subject.iot_dps_device_enrollment_list(None, mock_target['entity'], resource_group, top) - args = serviceclient.call_args_list[0] - headers = args[0][1] - url = args[0][0].url - method = args[0][0].method + request = serviceclient.calls[0].request + headers = request.headers + url = request.url + method = request.method assert str(headers.get("x-ms-max-item-count")) == str(top) assert "{}/enrollments/query?".format(mock_target['entity']) in url @@ -503,20 +594,23 @@ def test_enrollment_list_error(self, serviceclient_generic_error): class TestEnrollmentDelete(): @pytest.fixture(params=[204]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - response = mocker.MagicMock(name='response') - del response._attribute_map - response.status_code = request.param - service_client.return_value = response - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.DELETE, + url="https://{}/enrollments/{}".format(mock_target['entity'], enrollment_id), + body='{}', + status=request.param, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response def test_enrollment_delete(self, serviceclient): subject.iot_dps_device_enrollment_delete(None, enrollment_id, mock_target['entity'], resource_group) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method + request = serviceclient.calls[0].request + url = request.url + method = request.method assert "{}/enrollments/{}?".format(mock_target['entity'], enrollment_id) in url assert method == 'DELETE' @@ -566,10 +660,16 @@ def generate_enrollment_group_create_req(iot_hub_host_name=None, class TestEnrollmentGroupCreate(): @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.PUT, + url="https://{}/enrollmentGroups/{}".format(mock_target['entity'], enrollment_id), + body='{}', + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response @pytest.mark.parametrize("req", [ (generate_enrollment_group_create_req(primary_key='primarykey', @@ -627,12 +727,12 @@ def test_enrollment_group_create(self, serviceclient, req): req['edge_enabled'], req['webhook_url'], req['api_version']) - args = serviceclient.call_args - url = args[0][0].url + request = serviceclient.calls[0].request + url = request.url assert "{}/enrollmentGroups/{}?".format(mock_target['entity'], enrollment_id) in url - assert args[0][0].method == 'PUT' + assert request.method == 'PUT' - body = args[0][2] + body = json.loads(request.body) assert body['enrollmentGroupId'] == req['enrollment_id'] if req['certificate_path']: assert body['attestation']['type'] == 'x509' @@ -700,7 +800,7 @@ def test_enrollment_group_create(self, serviceclient, req): (generate_enrollment_group_create_req(allocation_policy='static', iot_hubs='hub1 hub2')), (generate_enrollment_group_create_req(iot_hubs='hub1 hub2')) ]) - def test_enrollment_group_create_invalid_args(self, serviceclient, req): + def test_enrollment_group_create_invalid_args(self, req): with pytest.raises(CLIError): subject.iot_dps_device_enrollment_group_create(None, req['enrollment_id'], @@ -808,14 +908,26 @@ def generate_enrollment_group_update_req(iot_hub_host_name=None, class TestEnrollmentGroupUpdate(): @pytest.fixture(params=[(200, generate_enrollment_group_show(), 200)]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - test_side_effect = [ - build_mock_response(mocker, request.param[0], request.param[1]), - build_mock_response(mocker, request.param[2]) - ] - service_client.side_effect = test_side_effect - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + # Initial GET + mocked_response.add( + method=responses.GET, + url="https://{}/enrollmentGroups/{}".format(mock_target['entity'], enrollment_id), + body=json.dumps(generate_enrollment_group_show()), + status=200, + content_type="application/json", + match_querystring=False, + ) + # Update PUT + mocked_response.add( + method=responses.PUT, + url="https://{}/enrollmentGroups/{}".format(mock_target['entity'], enrollment_id), + body=json.dumps(generate_enrollment_group_show()), + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response @pytest.mark.parametrize("req", [ (generate_enrollment_group_update_req(etag=etag, secondary_certificate_path='someOtherCertPath')), @@ -864,14 +976,19 @@ def test_enrollment_group_update(self, serviceclient, req): req['edge_enabled'], req['webhook_url'], req['api_version']) - # Index 1 is the update args - args = serviceclient.call_args_list[1] - url = args[0][0].url + # test initial GET + request = serviceclient.calls[0].request + url = request.url + assert "{}/enrollmentGroups/{}?".format(mock_target['entity'], enrollment_id) in url + assert request.method == 'GET' + + request = serviceclient.calls[1].request + url = request.url assert "{}/enrollmentGroups/{}?".format(mock_target['entity'], enrollment_id) in url - assert args[0][0].method == 'PUT' + assert request.method == 'PUT' - body = args[0][2] + body = json.loads(request.body) if not req['certificate_path']: if not req['root_ca_name'] and not req['secondary_root_ca_name']: assert body['attestation']['x509']['signingCertificates']['primary']['info'] is not None @@ -933,7 +1050,7 @@ def test_enrollment_group_update(self, serviceclient, req): (generate_enrollment_group_update_req(allocation_policy='static', iot_hubs='hub1 hub2')), (generate_enrollment_group_update_req(iot_hubs='hub1 hub2')) ]) - def test_enrollment_group_update_invalid_args(self, serviceclient, req): + def test_enrollment_group_update_invalid_args(self, req): with pytest.raises(CLIError): subject.iot_dps_device_enrollment_group_update(None, req['enrollment_id'], @@ -961,21 +1078,70 @@ def test_enrollment_group_update_invalid_args(self, serviceclient, req): class TestEnrollmentGroupShow(): @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, generate_enrollment_group_show()) - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.GET, + url="https://{}/enrollmentGroups/{}".format(mock_target['entity'], enrollment_id), + body=json.dumps(generate_enrollment_group_show()), + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response + + @pytest.fixture() + def serviceclient_attestation(self, mocked_response, fixture_gdcs, fixture_sas): + mocked_response.add( + method=responses.GET, + url="https://{}/enrollmentGroups/{}".format(mock_target['entity'], enrollment_id), + body=json.dumps(generate_enrollment_group_show(attestation=mock_symmetric_key_attestation)), + status=200, + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.POST, + url="https://{}/enrollmentGroups/{}/attestationmechanism".format(mock_target['entity'], enrollment_id), + body=json.dumps(mock_symmetric_key_attestation), + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response def test_enrollment_group_show(self, serviceclient): result = subject.iot_dps_device_enrollment_group_get(None, enrollment_id, mock_target['entity'], resource_group) - assert result.enrollment_group_id == enrollment_id - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method + assert result['enrollmentGroupId'] == enrollment_id + assert result['attestation'] + request = serviceclient.calls[0].request + url = request.url + method = request.method + + assert "{}/enrollmentGroups/{}?".format(mock_target['entity'], enrollment_id) in url + assert method == 'GET' + + def test_enrollment_group_show_with_keys(self, serviceclient_attestation): + result = subject.iot_dps_device_enrollment_group_get(None, enrollment_id, + mock_target['entity'], resource_group, show_keys=True) + assert result['enrollmentGroupId'] == enrollment_id + assert result['attestation'] + + request = serviceclient_attestation.calls[0].request + url = request.url + method = request.method + assert "{}/enrollmentGroups/{}?".format(mock_target['entity'], enrollment_id) in url assert method == 'GET' + request = serviceclient_attestation.calls[1].request + url = request.url + method = request.method + + assert "{}/enrollmentGroups/{}/attestationmechanism?".format(mock_target['entity'], enrollment_id) in url + assert method == 'POST' + def test_enrollment_group_show_error(self, serviceclient_generic_error): with pytest.raises(CLIError): subject.iot_dps_device_enrollment_group_get(None, enrollment_id, @@ -984,25 +1150,31 @@ def test_enrollment_group_show_error(self, serviceclient_generic_error): class TestEnrollmentGroupList(): @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, [generate_enrollment_group_show()]) - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.POST, + url="https://{}/enrollmentGroups/query?".format(mock_target['entity']), + body=json.dumps([generate_enrollment_group_show()]), + status=200, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response @pytest.mark.parametrize("top", [5, None]) def test_enrollment_group_list(self, serviceclient, top): result = subject.iot_dps_device_enrollment_group_list(None, mock_target['entity'], resource_group, top) - args = serviceclient.call_args_list[0] - headers = args[0][1] - url = args[0][0].url - method = args[0][0].method + request = serviceclient.calls[0].request + headers = request.headers + url = request.url + method = request.method assert "{}/enrollmentGroups/query?".format(mock_target['entity']) in url assert method == 'POST' assert json.dumps(result) assert str(headers.get("x-ms-max-item-count")) == str(top) - def test_enrollment_group_list_error(self, serviceclient_generic_error): + def test_enrollment_group_list_error(self): with pytest.raises(CLIError): subject.iot_dps_device_enrollment_group_list(None, mock_target['entity'], @@ -1011,21 +1183,27 @@ def test_enrollment_group_list_error(self, serviceclient_generic_error): class TestEnrollmentGroupDelete(): @pytest.fixture(params=[204]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.DELETE, + url="https://{}/enrollmentGroups/{}".format(mock_target['entity'], enrollment_id), + body='{}', + status=request.param, + content_type="application/json", + match_querystring=False, + ) + yield mocked_response def test_enrollment_group_delete(self, serviceclient): subject.iot_dps_device_enrollment_group_delete(None, enrollment_id, mock_target['entity'], resource_group) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method + request = serviceclient.calls[0].request + url = request.url + method = request.method assert "{}/enrollmentGroups/{}?".format(mock_target['entity'], enrollment_id) in url assert method == 'DELETE' - def test_enrollment_group_delete_error(self, serviceclient_generic_error): + def test_enrollment_group_delete_error(self): with pytest.raises(CLIError): subject.iot_dps_device_enrollment_group_delete(None, enrollment_id, mock_target['entity'], resource_group) @@ -1039,22 +1217,28 @@ def generate_registration_state_show(): class TestRegistrationShow(): @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, generate_registration_state_show()) - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.GET, + url="https://{}/registrations/{}?".format(mock_target['entity'], registration_id), + body=json.dumps(generate_registration_state_show()), + status=request.param, + content_type="application/json", + match_querystring=False + ) + yield mocked_response def test_registration_show(self, serviceclient): result = subject.iot_dps_registration_get(None, mock_target['entity'], resource_group, registration_id) - assert result.registration_id == registration_id - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method + assert result['registrationId'] == registration_id + request = serviceclient.calls[0].request + url = request.url + method = request.method assert "{}/registrations/{}?".format(mock_target['entity'], registration_id) in url assert method == 'GET' - def test_registration_show_error(self, serviceclient_generic_error): + def test_registration_show_error(self): with pytest.raises(CLIError): subject.iot_dps_registration_get(None, registration_id, mock_target['entity'], resource_group) @@ -1062,23 +1246,27 @@ def test_registration_show_error(self, serviceclient_generic_error): class TestRegistrationList(): @pytest.fixture(params=[200]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - result = [] - result.append(generate_registration_state_show()) - service_client.return_value = build_mock_response(mocker, request.param, result) - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.POST, + url="https://{}/registrations/{}/query?".format(mock_target['entity'], enrollment_id), + body=json.dumps([generate_registration_state_show()]), + status=request.param, + content_type="application/json", + match_querystring=False + ) + yield mocked_response def test_registration_list(self, serviceclient): subject.iot_dps_registration_list(None, mock_target['entity'], resource_group, enrollment_id) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method + request = serviceclient.calls[0].request + url = request.url + method = request.method assert "{}/registrations/{}/query?".format(mock_target['entity'], enrollment_id) in url assert method == 'POST' - def test_registration_list_error(self, serviceclient_generic_error): + def test_registration_list_error(self): with pytest.raises(CLIError): subject.iot_dps_registration_list(None, mock_target['entity'], resource_group, enrollment_id) @@ -1086,21 +1274,27 @@ def test_registration_list_error(self, serviceclient_generic_error): class TestRegistrationDelete(): @pytest.fixture(params=[204]) - def serviceclient(self, mocker, fixture_gdcs, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client + def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): + mocked_response.add( + method=responses.DELETE, + url="https://{}/registrations/{}".format(mock_target['entity'], registration_id), + body='{}', + status=request.param, + content_type="application/json", + match_querystring=False + ) + yield mocked_response def test_registration_delete(self, serviceclient): subject.iot_dps_registration_delete(None, mock_target['entity'], resource_group, registration_id) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method + request = serviceclient.calls[0].request + url = request.url + method = request.method assert "{}/registrations/{}?".format(mock_target['entity'], registration_id) in url assert method == 'DELETE' - def test_registration_delete_error(self, serviceclient_generic_error): + def test_registration_delete_error(self): with pytest.raises(CLIError): subject.iot_dps_registration_delete(None, registration_id, mock_target['entity'], resource_group) From bc7dcd0089c5e0789025966a3822c90f74fd30c4 Mon Sep 17 00:00:00 2001 From: Sapan Saxena <31940305+anusapan@users.noreply.github.com> Date: Tue, 15 Sep 2020 11:58:06 -0700 Subject: [PATCH 113/179] Implement convenience arguments for device update (#250) --- HISTORY.rst | 5 + azext_iot/_help.py | 6 + azext_iot/_params.py | 12 ++ azext_iot/commands.py | 6 +- azext_iot/operations/hub.py | 64 ++++++---- azext_iot/tests/test_iot_ext_int.py | 51 +++++++- azext_iot/tests/test_iot_ext_unit.py | 173 ++++++++++++++++++++++++++- 7 files changed, 289 insertions(+), 28 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 11790f457..7b4e38802 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +**IoT Hub updates** + +* Add convenience arguments for device update. + + 0.10.0 +++++++++++++++ diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 6fbae8bed..98ea6d0ea 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -200,9 +200,15 @@ text: > az iot hub device-identity update -d {device_id} -n {iothub_name} --set capabilities.iotEdge=true + - name: Turn on edge capabilities for device using convenience argument. + text: > + az iot hub device-identity update -d {device_id} -n {iothub_name} --ee - name: Disable device status text: > az iot hub device-identity update -d {device_id} -n {iothub_name} --set status=disabled + - name: Disable device status using convenience argument. + text: > + az iot hub device-identity update -d {device_id} -n {iothub_name} --status disabled - name: In one command text: > az iot hub device-identity update -d {device_id} -n {iothub_name} diff --git a/azext_iot/_params.py b/azext_iot/_params.py index e21213e39..93765b831 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -370,6 +370,18 @@ def load_arguments(self, _): help="Description for device status.", ) + with self.argument_context('iot hub device-identity update') as context: + context.argument( + "primary_key", + options_list=["--primary-key", "--pk"], + help="The primary symmetric shared access key stored in base64 format.", + ) + context.argument( + "secondary_key", + options_list=["--secondary-key", "--sk"], + 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", diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 1085b1fd6..d9ab7c05c 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -34,7 +34,11 @@ def load_command_table(self, _): cmd_group.command("list", "iot_device_list") cmd_group.command("delete", "iot_device_delete") cmd_group.generic_update_command( - "update", getter_name="iot_device_show", setter_name="iot_device_update" + "update", + getter_name="iot_device_show", + custom_func_type=iothub_ops, + setter_name="iot_device_update", + custom_func_name="update_iot_device_custom" ) cmd_group.command( diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 256bf2fa3..52e72a3f4 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -246,6 +246,38 @@ def _create_self_signed_cert(subject, valid_days, output_path=None): return create_self_signed_certificate(subject, valid_days, output_path) +def update_iot_device_custom(instance, edge_enabled=None, status=None, status_reason=None, + auth_method=None, primary_thumbprint=None, secondary_thumbprint=None, + primary_key=None, secondary_key=None): + if edge_enabled is not None: + instance['capabilities']['iotEdge'] = edge_enabled + if status is not None: + instance['status'] = status + if status_reason is not None: + instance['statusReason'] = status_reason + if auth_method is not None: + if auth_method == DeviceAuthType.shared_private_key.name: + auth = 'sas' + if (primary_key and not secondary_key) or (not primary_key and secondary_key): + raise CLIError("primary + secondary Key required with sas auth") + instance['authentication']['symmetricKey']['primaryKey'] = primary_key + instance['authentication']['symmetricKey']['secondaryKey'] = secondary_key + elif auth_method == DeviceAuthType.x509_thumbprint.name: + auth = 'selfSigned' + if not any([primary_thumbprint, secondary_thumbprint]): + raise CLIError("primary or secondary Thumbprint required with selfSigned auth") + if primary_thumbprint: + instance['authentication']['x509Thumbprint']['primaryThumbprint'] = primary_thumbprint + if secondary_thumbprint: + instance['authentication']['x509Thumbprint']['secondaryThumbprint'] = secondary_thumbprint + elif auth_method == DeviceAuthType.x509_ca.name: + auth = 'certificateAuthority' + else: + raise ValueError('Authorization method {} invalid.'.format(auth_method)) + instance['authentication']['type'] = auth + return instance + + def iot_device_update( cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None ): @@ -257,7 +289,15 @@ def iot_device_update( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - updated_device = _handle_device_update_params(parameters) + auth, pk, sk = _parse_auth(parameters) + updated_device = _assemble_device( + parameters['deviceId'], + auth, parameters['capabilities']['iotEdge'], + pk, + sk, + parameters['status'].lower(), + parameters.get('statusReason') + ) etag = parameters.get("etag", None) if etag: headers = {} @@ -272,28 +312,6 @@ def iot_device_update( raise CLIError(err) -def _handle_device_update_params(parameters): - status = parameters["status"].lower() - possible_status = ["enabled", "disabled"] - if status not in possible_status: - raise CLIError("status must be one of {}".format(possible_status)) - - edge = parameters["capabilities"].get("iotEdge") - if not isinstance(edge, bool): - raise CLIError("capabilities.iotEdge is of type bool") - - auth, pk, sk = _parse_auth(parameters) - return _assemble_device( - parameters["deviceId"], - auth, - edge, - pk, - sk, - status, - parameters.get("statusReason"), - ) - - def iot_device_delete( cmd, device_id, hub_name=None, resource_group_name=None, login=None ): diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 4503f4d46..e09088d45 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -444,8 +444,55 @@ def test_hub_devices(self): ) self.cmd( - '''iot hub device-identity update -d {} -n {} -g {} --set authentication.symmetricKey.primaryKey="" - authentication.symmetricKey.secondaryKey=""'''.format( + "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( edge_device_ids[1], LIVE_HUB, LIVE_RG ), checks=[ diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index faf9ea762..565aa0630 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -359,13 +359,15 @@ def test_device_create_error(self, serviceclient_generic_error, req): def generate_device_show(**kvp): payload = { "authentication": { - "symmetricKey": {"primaryKey": "123", "secondaryKey": "321"}, - "type": "sas", + "symmetricKey": {"primaryKey": None, "secondaryKey": None}, + "x509Thumbprint": {"primaryThumbprint": None, "secondaryThumbprint": None}, + "type": "sas" }, "etag": "abcd", "capabilities": {"iotEdge": True}, "deviceId": device_id, "status": "disabled", + "statusReason": "unknown reason", } for k in kvp: if payload.get(k): @@ -373,6 +375,28 @@ def generate_device_show(**kvp): return payload +def device_update_con_arg( + edge_enabled=None, + status=None, + status_reason=None, + auth_method=None, + primary_thumbprint=None, + secondary_thumbprint=None, + primary_key=None, + secondary_key=None +): + return { + "edge_enabled": edge_enabled, + "status": status, + "status_reason": status_reason, + "auth_method": auth_method, + "primary_thumbprint": primary_thumbprint, + "secondary_thumbprint": secondary_thumbprint, + "primary_key": primary_key, + "secondary_key": secondary_key + } + + class TestDeviceUpdate: @pytest.fixture(params=[200]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): @@ -384,6 +408,8 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): @pytest.mark.parametrize( "req", [ + (generate_device_show(capabilities={"iotEdge": False})), + (generate_device_show(status="enabled")), ( generate_device_show( authentication={ @@ -431,6 +457,149 @@ def test_device_update(self, fixture_cmd, serviceclient, req): headers = args[0][0].headers assert headers["If-Match"] == '"{}"'.format(req["etag"]) + @pytest.mark.parametrize( + "req, arg", + [ + ( + generate_device_show( + capabilities={"iotEdge": False} + ), + device_update_con_arg( + edge_enabled=True + ) + ), + ( + generate_device_show( + status="disabled" + ), + device_update_con_arg( + status="enabled" + ) + ), + ( + generate_device_show(), + device_update_con_arg( + status_reason="test" + ) + ), + ( + generate_device_show(), + device_update_con_arg( + auth_method="shared_private_key", + primary_key="primarykeyUpdated", + secondary_key="secondarykeyUpdated" + ) + ), + ( + generate_device_show( + authentication={ + "type": "selfSigned", + "symmetricKey": {"primaryKey": None, "secondaryKey": None}, + "x509Thumbprint": {"primaryThumbprint": "123", "secondaryThumbprint": "321"}, + } + ), + device_update_con_arg( + auth_method="shared_private_key", + primary_key="primary_key", + secondary_key="secondary_key" + ) + ), + ( + generate_device_show( + authentication={ + "type": "certificateAuthority", + "symmetricKey": {"primaryKey": None, "secondaryKey": None}, + "x509Thumbprint": {"primaryThumbprint": None, "secondaryThumbprint": None}, + } + ), + device_update_con_arg( + auth_method="x509_thumbprint", + primary_thumbprint="primary_thumbprint", + secondary_thumbprint="secondary_thumbprint" + ) + ), + ( + generate_device_show(), + device_update_con_arg( + auth_method="x509_ca", + ) + ), + ] + ) + def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): + instance = subject.update_iot_device_custom( + req, + arg["edge_enabled"], + arg["status"], + arg["status_reason"], + arg['auth_method'], + arg['primary_thumbprint'], + arg['secondary_thumbprint'], + arg['primary_key'], + arg["secondary_key"] + ) + + if arg["edge_enabled"]: + assert instance["capabilities"]["iotEdge"] == arg["edge_enabled"] + if arg["status"]: + assert instance["status"] == arg["status"] + if arg["status_reason"]: + assert instance['statusReason'] == arg["status_reason"] + if arg["auth_method"]: + if arg["auth_method"] == "shared_private_key": + assert instance['authentication']['type'] == "sas" + instance['authentication']['symmetricKey']['primaryKey'] == arg['primary_key'] + instance['authentication']['symmetricKey']['secondaryKey'] == arg['secondary_key'] + if arg["auth_method"] == "x509_thumbprint": + assert instance['authentication']['type'] == "selfSigned" + if arg['primary_thumbprint']: + instance['authentication']['x509Thumbprint']['primaryThumbprint'] = arg['primary_thumbprint'] + if arg['secondary_thumbprint']: + instance['authentication']['x509Thumbprint']['secondaryThumbprint'] = arg['secondary_thumbprint'] + if arg['auth_method'] == "x509_ca": + assert instance['authentication']['type'] == "certificateAuthority" + + @pytest.mark.parametrize( + "req, arg, exp", + [ + ( + generate_device_show(), + device_update_con_arg( + auth_method="shared_private_key", + primary_key="primarykeyUpdated", + ), + CLIError + ), + ( + generate_device_show(), + device_update_con_arg( + auth_method="x509_thumbprint", + ), + CLIError + ), + ( + generate_device_show(), + device_update_con_arg( + auth_method="Unknown", + ), + ValueError + ), + ] + ) + def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): + with pytest.raises(exp): + subject.update_iot_device_custom( + req, + arg["edge_enabled"], + arg["status"], + arg["status_reason"], + arg['auth_method'], + arg['primary_thumbprint'], + arg['secondary_thumbprint'], + arg['primary_key'], + arg["secondary_key"] + ) + @pytest.mark.parametrize( "req, exp", [ From ee90ecf1ecb58f5ba53dbe9928233b7dd1f6087f Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 15 Sep 2020 11:59:19 -0700 Subject: [PATCH 114/179] Update HISTORY.rst --- HISTORY.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7b4e38802..de62f9d6e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,24 +3,23 @@ Release History =============== +0.10.0 ++++++++++++++++ + **IoT Hub updates** * Add convenience arguments for device update. +**IoT DPS updates** -0.10.0 -+++++++++++++++ +* Added --show-keys argument to `dps enrollment show` and `dps enrollment-group show` to include full attestation information for symmetric key enrollments and enrollment groups +* Regenerated 2019-03-31 DPS Service SDK **Breaking Changes** * `az iot dps enrollment show` and `az iot dps enrollment-group show` now return raw service results instead of deserialized models. This means that some properties that were previously returned as `null` for these commands will no longer be returned, possibly causing a breaking change. -**IoT DPS updates** - -* Added --show-keys argument to `dps enrollment show` and `dps enrollment-group show` to include full attestation information for symmetric key enrollments and enrollment groups -* Regenerated 2019-03-31 DPS Service SDK - 0.9.9 +++++++++++++++ From d3e70880cb2972b644055ee32765f403c569c84f Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 15 Sep 2020 12:05:21 -0700 Subject: [PATCH 115/179] Update create-release.yml for Azure Pipelines --- .azure-devops/create-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index 7ed3cfacb..d39db27bb 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -46,7 +46,7 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.6.x' - runUnitTestsOnly: $[coalesce(variables['runUnitTestsOnly'], 'false')] + runUnitTestsOnly: 'false' - script: 'pip install .' displayName: 'Install the whl locally' From 7580f94cb18489cd5b6fdaa9e3c83a6ff0720b1a Mon Sep 17 00:00:00 2001 From: valluriraj Date: Tue, 15 Sep 2020 17:52:00 -0700 Subject: [PATCH 116/179] Central monitor events IT fix (#252) --- azext_iot/tests/test_iot_central_int.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/test_iot_central_int.py index 6f17cd28f..335af0eee 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/test_iot_central_int.py @@ -103,6 +103,8 @@ def test_central_monitor_events_deprecated(self): device_client = helpers.dps_connect_device(device_id, credentials) + enqueued_time = utility.calculate_millisec_since_unix_epoch_utc() - 10000 + payload = {"Bool": True} msg = Message( data=json.dumps(payload), @@ -111,8 +113,6 @@ def test_central_monitor_events_deprecated(self): ) device_client.send_message(msg) - enqueued_time = utility.calculate_millisec_since_unix_epoch_utc() - 10000 - # Test with invalid app-id self.cmd( "iot central app monitor-events --app-id {} -y".format(APP_ID + "zzz"), @@ -134,6 +134,8 @@ def test_central_monitor_events(self): device_client = helpers.dps_connect_device(device_id, credentials) + enqueued_time = utility.calculate_millisec_since_unix_epoch_utc() - 10000 + payload = {"Bool": True} msg = Message( data=json.dumps(payload), @@ -142,8 +144,6 @@ def test_central_monitor_events(self): ) device_client.send_message(msg) - enqueued_time = utility.calculate_millisec_since_unix_epoch_utc() - 10000 - # Test with invalid app-id self.cmd( "iot central diagnostics monitor-events --app-id {} -y".format( From 029cf2c51fa7d4f15ff836dce2ba75bc5c53d732 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Wed, 23 Sep 2020 20:04:11 -0700 Subject: [PATCH 117/179] PnP Runtime - Regenerated 2020-09-30 SDK for IotHub Gateway Service API (#253) * Regenerated 2020-09-30 IotHub Gateway Service API for digital twin operations * Added history entry --- HISTORY.rst | 8 ++++++++ .../iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py | 2 +- .../pnp_runtime/operations/digital_twin_operations.py | 4 ++-- azext_iot/sdk/iothub/pnp_runtime/version.py | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index de62f9d6e..f16d01d30 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,14 @@ Release History =============== +0.11.0 ++++++++++++++++ + +**IoT Plug-and-Play updates** + +* Regenerated runtime SDK for API version 2020-09-30 + + 0.10.0 +++++++++++++++ diff --git a/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py b/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py index 93ca28267..e5f7a1273 100644 --- a/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py +++ b/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py @@ -83,7 +83,7 @@ def __init__( super(IotHubGatewayServiceAPIs, self).__init__(self.config.credentials, self.config) client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2020-06-30-preview' + self.api_version = '2020-09-30' self._serialize = Serializer(client_models) self._deserialize = Deserializer(client_models) diff --git a/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py b/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py index c413aa7bd..9c66420a2 100644 --- a/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py +++ b/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py @@ -23,7 +23,7 @@ class DigitalTwinOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the Api. Constant value: "2020-06-30-preview". + :ivar api_version: The API version to use for the request. Constant value: "2020-09-30". """ def __init__(self, client, config, serializer, deserializer): @@ -31,7 +31,7 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-06-30-preview" + self.api_version = "2020-09-30" self.config = config diff --git a/azext_iot/sdk/iothub/pnp_runtime/version.py b/azext_iot/sdk/iothub/pnp_runtime/version.py index e2d2d7dc8..f4d73cc97 100644 --- a/azext_iot/sdk/iothub/pnp_runtime/version.py +++ b/azext_iot/sdk/iothub/pnp_runtime/version.py @@ -9,5 +9,5 @@ # regenerated. # -------------------------------------------------------------------------- -VERSION = "2020-06-30-preview" +VERSION = "2020-09-30" From 262cf9a01454b4515c97e12baea50832a2676e68 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Tue, 29 Sep 2020 12:26:34 -0700 Subject: [PATCH 118/179] GA for PnP Runtime (#256) Removed preview tag from `az iot pnp twin` command group Version update to 0.10.1 --- HISTORY.rst | 3 ++- azext_iot/constants.py | 2 +- azext_iot/iothub/command_map.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f16d01d30..f543888c6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,12 +3,13 @@ Release History =============== -0.11.0 +0.10.1 +++++++++++++++ **IoT Plug-and-Play updates** * Regenerated runtime SDK for API version 2020-09-30 +* GA release for `az iot pnp twin` command group 0.10.0 diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 87f1fb11a..2a77ffce3 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.0" +VERSION = "0.10.1" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/iothub/command_map.py b/azext_iot/iothub/command_map.py index e640fcb20..bc85166b3 100644 --- a/azext_iot/iothub/command_map.py +++ b/azext_iot/iothub/command_map.py @@ -25,7 +25,7 @@ def load_iothub_commands(self, _): cmd_group.command("list", "job_list") cmd_group.command("cancel", "job_cancel") - with self.command_group("iot pnp twin", command_type=pnp_runtime_ops, is_preview=True) as cmd_group: + with self.command_group("iot pnp twin", command_type=pnp_runtime_ops) as cmd_group: cmd_group.command("invoke-command", "invoke_device_command") cmd_group.show_command("show", "get_digital_twin") cmd_group.command("update", "patch_digital_twin") From f95484c58afbf18f424f33da2d39d6ec96209728 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 29 Sep 2020 17:25:40 -0700 Subject: [PATCH 119/179] Update create-release.yml for Azure Pipelines --- .azure-devops/create-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index d39db27bb..ac58ca9d7 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -46,7 +46,7 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.6.x' - runUnitTestsOnly: 'false' + runUnitTestsOnly: '' - script: 'pip install .' displayName: 'Install the whl locally' From ce1e83e557516cb222984e00ecacf64250ab0f55 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 1 Oct 2020 11:02:21 -0700 Subject: [PATCH 120/179] Remove --top constraints from config list commands. (#257) --- HISTORY.rst | 9 ++- azext_iot/_params.py | 4 +- azext_iot/operations/hub.py | 80 ++++++++++++------- .../configurations/test_iot_config_int.py | 24 ------ .../configurations/test_iot_config_unit.py | 18 +++-- 5 files changed, 72 insertions(+), 63 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f543888c6..81cef2e1c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,8 +8,13 @@ Release History **IoT Plug-and-Play updates** -* Regenerated runtime SDK for API version 2020-09-30 -* GA release for `az iot pnp twin` command group +* Regenerated PnP runtime SDK to API version 2020-09-30 +* All `az iot pnp` commands still remain under preview and are subject to change or deletion. + +** IoT Hub updates** + +* All configuration/edge deployment list operations no longer have a default top. By default all configuration entities will be returned. + Existing --top input should not be affected. 0.10.0 diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 93765b831..f3194a86a 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -696,7 +696,7 @@ def load_arguments(self, _): "top", options_list=["--top"], type=int, - help="Maximum number of configurations to return.", + help="Maximum number of configurations to return. By default all configurations are returned.", ) with self.argument_context("iot edge") as context: @@ -735,7 +735,7 @@ def load_arguments(self, _): "top", options_list=["--top"], type=int, - help="Maximum number of deployments to return.", + help="Maximum number of deployments to return. By default all deployments are returned.", ) context.argument( "layered", diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 52e72a3f4..9c24cd036 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -246,35 +246,51 @@ def _create_self_signed_cert(subject, valid_days, output_path=None): return create_self_signed_certificate(subject, valid_days, output_path) -def update_iot_device_custom(instance, edge_enabled=None, status=None, status_reason=None, - auth_method=None, primary_thumbprint=None, secondary_thumbprint=None, - primary_key=None, secondary_key=None): +def update_iot_device_custom( + instance, + edge_enabled=None, + status=None, + status_reason=None, + auth_method=None, + primary_thumbprint=None, + secondary_thumbprint=None, + primary_key=None, + secondary_key=None, +): if edge_enabled is not None: - instance['capabilities']['iotEdge'] = edge_enabled + instance["capabilities"]["iotEdge"] = edge_enabled if status is not None: - instance['status'] = status + instance["status"] = status if status_reason is not None: - instance['statusReason'] = status_reason + instance["statusReason"] = status_reason if auth_method is not None: if auth_method == DeviceAuthType.shared_private_key.name: - auth = 'sas' - if (primary_key and not secondary_key) or (not primary_key and secondary_key): + auth = "sas" + if (primary_key and not secondary_key) or ( + not primary_key and secondary_key + ): raise CLIError("primary + secondary Key required with sas auth") - instance['authentication']['symmetricKey']['primaryKey'] = primary_key - instance['authentication']['symmetricKey']['secondaryKey'] = secondary_key + instance["authentication"]["symmetricKey"]["primaryKey"] = primary_key + instance["authentication"]["symmetricKey"]["secondaryKey"] = secondary_key elif auth_method == DeviceAuthType.x509_thumbprint.name: - auth = 'selfSigned' + auth = "selfSigned" if not any([primary_thumbprint, secondary_thumbprint]): - raise CLIError("primary or secondary Thumbprint required with selfSigned auth") + raise CLIError( + "primary or secondary Thumbprint required with selfSigned auth" + ) if primary_thumbprint: - instance['authentication']['x509Thumbprint']['primaryThumbprint'] = primary_thumbprint + instance["authentication"]["x509Thumbprint"][ + "primaryThumbprint" + ] = primary_thumbprint if secondary_thumbprint: - instance['authentication']['x509Thumbprint']['secondaryThumbprint'] = secondary_thumbprint + instance["authentication"]["x509Thumbprint"][ + "secondaryThumbprint" + ] = secondary_thumbprint elif auth_method == DeviceAuthType.x509_ca.name: - auth = 'certificateAuthority' + auth = "certificateAuthority" else: - raise ValueError('Authorization method {} invalid.'.format(auth_method)) - instance['authentication']['type'] = auth + raise ValueError("Authorization method {} invalid.".format(auth_method)) + instance["authentication"]["type"] = auth return instance @@ -291,12 +307,13 @@ def iot_device_update( try: auth, pk, sk = _parse_auth(parameters) updated_device = _assemble_device( - parameters['deviceId'], - auth, parameters['capabilities']['iotEdge'], + parameters["deviceId"], + auth, + parameters["capabilities"]["iotEdge"], pk, sk, - parameters['status'].lower(), - parameters.get('statusReason') + parameters["status"].lower(), + parameters.get("statusReason"), ) etag = parameters.get("etag", None) if etag: @@ -1144,7 +1161,7 @@ def _iot_hub_configuration_show(target, config_id): def iot_hub_configuration_list( - cmd, hub_name=None, top=10, resource_group_name=None, login=None + cmd, hub_name=None, top=None, resource_group_name=None, login=None ): result = _iot_hub_configuration_list( cmd, @@ -1156,13 +1173,16 @@ def iot_hub_configuration_list( filtered = [ c for c in result - if (c["content"].get("deviceContent") or c["content"].get("moduleContent")) + if ( + c["content"].get("deviceContent") is not None + or c["content"].get("moduleContent") is not None + ) ] - return filtered[:top] + return filtered[:top] # list[:None] == list[:len(list)] def iot_edge_deployment_list( - cmd, hub_name=None, top=10, resource_group_name=None, login=None + cmd, hub_name=None, top=None, resource_group_name=None, login=None ): result = _iot_hub_configuration_list( cmd, @@ -1172,14 +1192,14 @@ def iot_edge_deployment_list( login=login, ) - filtered = [c for c in result if c["content"].get("modulesContent")] - return filtered[:top] + filtered = [c for c in result if c["content"].get("modulesContent") is not None] + return filtered[:top] # list[:None] == list[:len(list)] def _iot_hub_configuration_list( - cmd, hub_name=None, top=10, resource_group_name=None, login=None + cmd, hub_name=None, top=None, resource_group_name=None, login=None ): - top = _process_top(top, upper_limit=100) + top = _process_top(top) discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -1863,7 +1883,7 @@ def iot_c2d_message_receive( login=None, abandon=None, complete=None, - reject=None + reject=None, ): ack = None ack_vals = [abandon, complete, reject] 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 65b18f1c5..68aba4382 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_int.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_int.py @@ -382,18 +382,6 @@ def test_edge_deployments(self): checks=config_list_check, ) - # Error top of -1 does not work with configurations - self.cmd( - "iot edge deployment list -n {} -g {} --top -1".format(LIVE_HUB, LIVE_RG), - expect_failure=True, - ) - - # Error max top of 100 with configurations - self.cmd( - "iot edge deployment list -n {} -g {} --top 101".format(LIVE_HUB, LIVE_RG), - expect_failure=True, - ) - # Explicitly delete an edge deployment self.cmd( "iot edge deployment delete -d {} -n {} -g {}".format( @@ -685,18 +673,6 @@ def test_device_configurations(self): checks=config_list_check, ) - # Error top of -1 does not work with configurations - self.cmd( - "iot hub configuration list -n {} -g {} --top -1".format(LIVE_HUB, LIVE_RG), - expect_failure=True, - ) - - # Error max top of 100 with configurations - self.cmd( - "iot hub configuration list -n {} -g {} --top 101".format(LIVE_HUB, LIVE_RG), - expect_failure=True, - ) - # Explicitly delete an ADM configuration self.cmd( "iot hub configuration delete -c {} -n {} -g {}".format( diff --git a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py index 8d9a9606c..e681bccaf 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py @@ -678,7 +678,7 @@ def test_config_update_error(self, fixture_cmd, serviceclient_generic_error): class TestConfigList: - @pytest.fixture(params=[10, 0, 20]) + @pytest.fixture(params=[10, 0, 1000]) def service_client(self, mocked_response, fixture_ghcs, request): result = [] size = request.param @@ -711,7 +711,7 @@ def service_client(self, mocked_response, fixture_ghcs, request): mocked_response.expected_size = size yield mocked_response - @pytest.mark.parametrize("top", [1, 100]) + @pytest.mark.parametrize("top", [1, 100, 1000, None]) def test_config_list(self, fixture_cmd, service_client, top): result = subject.iot_hub_configuration_list( cmd=fixture_cmd, hub_name=mock_target["entity"], top=top @@ -722,9 +722,13 @@ def test_config_list(self, fixture_cmd, service_client, top): assert len(result) == top or len(result) == service_client.expected_size * 2 list_request = service_client.calls[0].request - assert "top={}".format(top) in list_request.url - @pytest.mark.parametrize("top", [1, 10]) + if top: + assert "top={}".format(top) in list_request.url + else: + assert not "top={}".format(top) in list_request.url + + @pytest.mark.parametrize("top", [1, 100, 1000, None]) def test_deployment_list(self, fixture_cmd, service_client, top): result = subject.iot_edge_deployment_list( cmd=fixture_cmd, hub_name=mock_target["entity"], top=top @@ -734,7 +738,11 @@ def test_deployment_list(self, fixture_cmd, service_client, top): assert len(result) == top or len(result) == service_client.expected_size list_request = service_client.calls[0].request - assert "top={}".format(top) in list_request.url + + if top: + assert "top={}".format(top) in list_request.url + else: + assert not "top={}".format(top) in list_request.url @pytest.mark.parametrize("top", [-1, 0, 101]) def test_config_list_invalid_args(self, fixture_cmd, top): From f32fefd8ddd5e2bc5f143b050103329465e11c65 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 1 Oct 2020 12:43:15 -0700 Subject: [PATCH 121/179] Update HISTORY.rst --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 81cef2e1c..7f918f745 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,7 +11,7 @@ Release History * Regenerated PnP runtime SDK to API version 2020-09-30 * All `az iot pnp` commands still remain under preview and are subject to change or deletion. -** IoT Hub updates** +**IoT Hub updates** * All configuration/edge deployment list operations no longer have a default top. By default all configuration entities will be returned. Existing --top input should not be affected. From 67dfd0eb31a31cc189ae834e634a67e29a6771d4 Mon Sep 17 00:00:00 2001 From: Sapan Saxena <31940305+anusapan@users.noreply.github.com> Date: Thu, 1 Oct 2020 13:29:51 -0700 Subject: [PATCH 122/179] Implement regenerate key command for the device (#254) * Implement renewkey command for the device * Update history * Resolved PR comments --- HISTORY.rst | 8 +++ azext_iot/_help.py | 10 ++++ azext_iot/_params.py | 9 ++++ azext_iot/commands.py | 1 + azext_iot/common/shared.py | 9 ++++ azext_iot/common/utility.py | 9 ++++ azext_iot/operations/hub.py | 78 +++++++++++++++++++++------- azext_iot/tests/test_iot_ext_int.py | 28 ++++++++++ azext_iot/tests/test_iot_ext_unit.py | 78 +++++++++++++++++++++++++++- 9 files changed, 208 insertions(+), 22 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7f918f745..299d8e3d5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,14 @@ Release History =============== +0.10.2 ++++++++++++++++ + +**IoT Hub updates** + +* Add command to regenerate keys for the device. + + 0.10.1 +++++++++++++++ diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 98ea6d0ea..ffd93566f 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -215,6 +215,16 @@ --set status=disabled capabilities.iotEdge=true """ +helps[ + "iot hub device-identity renew-key" +] = """ + type: command + short-summary: Regenerate keys of a device with sas authentication of an IoT hub. + examples: + - name: Regenerate primary key of a device of an IoT hub. + text: az iot hub device-identity renew-key -d {device_id} -n {iothub_name} --rk Primary +""" + helps[ "iot hub device-identity delete" ] = """ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index f3194a86a..79ff33e79 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -31,6 +31,7 @@ JobCreateType, JobStatusType, AuthenticationType, + RenewKeyType, ) from azext_iot._validators import mode2_iot_login_handler from azext_iot.assets.user_messages import info_param_properties_device @@ -399,6 +400,14 @@ def load_arguments(self, _): help="Child device list (comma separated) includes only non-edge devices.", ) + with self.argument_context('iot hub device-identity renew-key') as context: + context.argument( + "regenerate_key", + options_list=["--renew-key", "--rk"], + arg_type=get_enum_type(RenewKeyType), + help="Regenerate keys" + ) + with self.argument_context("iot hub device-identity export") as context: context.argument( "blob_container_uri", diff --git a/azext_iot/commands.py b/azext_iot/commands.py index d9ab7c05c..2c9a49f5b 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -40,6 +40,7 @@ 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_renew') cmd_group.command( "show-connection-string", diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index c27e902f1..42ec8ee0e 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -214,3 +214,12 @@ class AuthenticationType(Enum): keyBased = "key" identityBased = "identity" + + +class RenewKeyType(Enum): + """ + Type of the RegenerateKey for the device. + """ + primary = "primary" + secondary = "secondary" + swap = "swap" diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index a939d5855..954eec775 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -18,6 +18,7 @@ import re import hmac import hashlib +import random from threading import Event, Thread from datetime import datetime @@ -515,3 +516,11 @@ def compute_device_key(primary_key, registration_id): ).digest() ) return device_key + + +def generate_key(byte_length=32): + key = "" + while byte_length > 0: + key += chr(random.randrange(1, 128)) + byte_length -= 1 + return base64.b64encode(key.encode()).decode("utf-8") diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 9c24cd036..2144b8885 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -24,6 +24,7 @@ ConfigType, KeyType, SettleType, + RenewKeyType ) from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot.common.utility import ( @@ -35,6 +36,7 @@ init_monitoring, process_json_arg, ensure_min_version, + generate_key ) from azext_iot._factory import SdkResolver, CloudError from azext_iot.operations.generic import _execute_query, _process_top @@ -301,32 +303,34 @@ def iot_device_update( target = discovery.get_target( hub_name=hub_name, resource_group_name=resource_group_name, login=login ) + + auth, pk, sk = _parse_auth(parameters) + updated_device = _assemble_device( + parameters['deviceId'], + auth, parameters['capabilities']['iotEdge'], + pk, + sk, + parameters['status'].lower(), + parameters.get('statusReason') + ) + updated_device.etag = parameters.get("etag", "*") + return _iot_device_update(target, device_id, updated_device) + + +def _iot_device_update(target, device_id, device): resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - auth, pk, sk = _parse_auth(parameters) - updated_device = _assemble_device( - parameters["deviceId"], - auth, - parameters["capabilities"]["iotEdge"], - pk, - sk, - parameters["status"].lower(), - parameters.get("statusReason"), - ) - etag = parameters.get("etag", None) - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.registry_manager.create_or_update_device( - id=device_id, device=updated_device, custom_headers=headers - ) - raise LookupError("device etag not found.") + headers = {} + headers["If-Match"] = '"{}"'.format(device.etag) + return service_sdk.registry_manager.create_or_update_device( + id=device_id, + device=device, + custom_headers=headers + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def iot_device_delete( @@ -357,6 +361,40 @@ def iot_device_delete( raise CLIError(err) +def iot_device_key_renew(cmd, hub_name, device_id, regenerate_key, 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 + ) + device = _iot_device_show(target, device_id) + if (device["authentication"]["type"] != "sas"): + raise CLIError("Device authentication should be of type sas") + + pk = device["authentication"]["symmetricKey"]["primaryKey"] + sk = device["authentication"]["symmetricKey"]["secondaryKey"] + if regenerate_key == RenewKeyType.primary.value: + pk = generate_key() + if regenerate_key == RenewKeyType.secondary.value: + sk = generate_key() + if regenerate_key == RenewKeyType.swap.value: + temp = pk + pk = sk + sk = temp + + updated_device = _assemble_device( + device["deviceId"], + device["authentication"]["type"], + device["capabilities"]["iotEdge"], + pk, + sk, + device["status"].lower(), + device["statusReason"], + device.get("deviceScope", None) + ) + updated_device.etag = device["etag"] + return _iot_device_update(target, device_id, updated_device) + + def iot_device_get_parent( cmd, device_id, hub_name=None, resource_group_name=None, login=None ): diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index e09088d45..edbf395cd 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -517,6 +517,34 @@ def test_hub_devices(self): ], ) + # Test 'az iot hub device renew-key' + device = self.cmd( + '''iot hub device-identity renew-key -d {} -n {} -g {} --rk 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 {} --rk 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 {} --rk 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] ) diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index 565aa0630..f18a09560 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -615,8 +615,7 @@ def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): ), CLIError, ), - (generate_device_show(authentication={"type": "doesnotexist"}), CLIError), - (generate_device_show(etag=None), CLIError), + (generate_device_show(authentication={"type": "doesnotexist"}), CLIError) ], ) def test_device_update_invalid_args(self, serviceclient, req, exp): @@ -639,6 +638,81 @@ def test_device_update_error(self, serviceclient_generic_error, req): ) +class TestDeviceRegenerateKey: + @pytest.fixture(params=[200]) + def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): + service_client = mocker.patch(path_service_client) + kvp = {} + kvp.setdefault( + "authentication", + { + "symmetricKey": { + "primaryKey": "123", + "secondaryKey": "321" + }, + "type": "sas" + } + ) + test_side_effect = [ + build_mock_response(mocker, 200, generate_device_show(**kvp)), + build_mock_response(mocker, 200, {}) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) + def test_device_key_renew(self, fixture_cmd, serviceclient, req): + subject.iot_device_key_renew( + fixture_cmd, mock_target["entity"], device_id, req + ) + args = serviceclient.call_args + assert ( + "{}/devices/{}?".format(mock_target["entity"], device_id) in args[0][0].url + ) + assert args[0][0].method == "PUT" + + body = json.loads(args[0][0].body) + if(req == "primary"): + assert body["authentication"]["symmetricKey"]["primaryKey"] != "123" + if(req == "secondary"): + assert body["authentication"]["symmetricKey"]["secondaryKey"] != "321" + if(req == "swap"): + assert body["authentication"]["symmetricKey"]["primaryKey"] == "321" + assert body["authentication"]["symmetricKey"]["secondaryKey"] == "123" + + @pytest.fixture(params=[200]) + def serviceclient_invalid_args(self, mocker, fixture_ghcs, fixture_sas, request): + service_client = mocker.patch(path_service_client) + kvp = {} + kvp.setdefault("authentication", {"type": "test"}) + test_side_effect = [ + build_mock_response(mocker, 200, generate_device_show(**kvp)) + ] + service_client.side_effect = test_side_effect + return service_client + + @pytest.mark.parametrize( + "req, exp", + [ + ("primary", CLIError), + ("secondary", CLIError), + ("swap", CLIError) + ] + ) + def test_device_key_renew_invalid_args(self, fixture_cmd, serviceclient_invalid_args, req, exp): + with pytest.raises(exp): + subject.iot_device_key_renew( + fixture_cmd, mock_target["entity"], device_id, req + ) + + @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) + def test_device_key_renew_error(self, serviceclient_generic_error, req): + with pytest.raises(CLIError): + subject.iot_device_key_renew( + fixture_cmd, mock_target["entity"], device_id, req + ) + + class TestDeviceDelete: @pytest.fixture(params=[(200, 204)]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): From 92c29a43a886e0b5f2feb3de69faf001b086a116 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 1 Oct 2020 17:03:59 -0700 Subject: [PATCH 123/179] Consistent configuration top and tweak regenerate-key command syntax (#258) * Make consistent top behavior for configurations. Tweak regenerate-key command wording and arg syntax. * Fix unit tests. --- HISTORY.rst | 2 +- azext_iot/_help.py | 10 ++++++---- azext_iot/_params.py | 10 +++++----- azext_iot/commands.py | 3 +-- azext_iot/common/shared.py | 8 ++++---- azext_iot/constants.py | 2 +- azext_iot/operations/hub.py | 20 +++++++------------ .../configurations/test_iot_config_unit.py | 12 ++--------- azext_iot/tests/test_iot_ext_int.py | 12 +++++------ azext_iot/tests/test_iot_ext_unit.py | 12 +++++------ 10 files changed, 39 insertions(+), 52 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 299d8e3d5..d9153d3b0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,7 +8,7 @@ Release History **IoT Hub updates** -* Add command to regenerate keys for the device. +* Adds `az iot hub device-identity regenerate-key`. 0.10.1 diff --git a/azext_iot/_help.py b/azext_iot/_help.py index ffd93566f..5cdc6e6a8 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -216,13 +216,15 @@ """ helps[ - "iot hub device-identity renew-key" + "iot hub device-identity regenerate-key" ] = """ type: command - short-summary: Regenerate keys of a device with sas authentication of an IoT hub. + short-summary: Regenerate target keys of an IoT Hub device with sas authentication. examples: - - name: Regenerate primary key of a device of an IoT hub. - text: az iot hub device-identity renew-key -d {device_id} -n {iothub_name} --rk Primary + - name: Regenerate the primary key. + text: az iot hub device-identity regenerate-key -d {device_id} -n {iothub_name} --kt primary + - name: Swap the primary and secondary keys. + text: az iot hub device-identity regenerate-key -d {device_id} -n {iothub_name} --kt swap """ helps[ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 79ff33e79..5cab38d85 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -31,7 +31,7 @@ JobCreateType, JobStatusType, AuthenticationType, - RenewKeyType, + RegenerateKeyType, ) from azext_iot._validators import mode2_iot_login_handler from azext_iot.assets.user_messages import info_param_properties_device @@ -400,12 +400,12 @@ def load_arguments(self, _): help="Child device list (comma separated) includes only non-edge devices.", ) - with self.argument_context('iot hub device-identity renew-key') as context: + with self.argument_context('iot hub device-identity regenerate-key') as context: context.argument( "regenerate_key", - options_list=["--renew-key", "--rk"], - arg_type=get_enum_type(RenewKeyType), - help="Regenerate keys" + options_list=["--key-type", "--kt"], + arg_type=get_enum_type(RegenerateKeyType), + help="Target key type to regenerate." ) with self.argument_context("iot hub device-identity export") as context: diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 2c9a49f5b..66c3a82f1 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -40,8 +40,7 @@ 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_renew') - + cmd_group.command("regenerate-key", 'iot_device_key_regenerate') cmd_group.command( "show-connection-string", "iot_get_device_connection_string", diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index 42ec8ee0e..484a3f224 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -216,10 +216,10 @@ class AuthenticationType(Enum): identityBased = "identity" -class RenewKeyType(Enum): +class RegenerateKeyType(Enum): """ - Type of the RegenerateKey for the device. + Target key type for regeneration. """ - primary = "primary" - secondary = "secondary" + primary = KeyType.primary.value + secondary = KeyType.secondary.value swap = "swap" diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 2a77ffce3..00ae708b2 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.1" +VERSION = "0.10.2" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 2144b8885..f997bfbea 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -24,7 +24,7 @@ ConfigType, KeyType, SettleType, - RenewKeyType + RegenerateKeyType ) from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot.common.utility import ( @@ -361,7 +361,7 @@ def iot_device_delete( raise CLIError(err) -def iot_device_key_renew(cmd, hub_name, device_id, regenerate_key, resource_group_name=None, login=None): +def iot_device_key_regenerate(cmd, hub_name, device_id, regenerate_key, 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 @@ -372,11 +372,11 @@ def iot_device_key_renew(cmd, hub_name, device_id, regenerate_key, resource_grou pk = device["authentication"]["symmetricKey"]["primaryKey"] sk = device["authentication"]["symmetricKey"]["secondaryKey"] - if regenerate_key == RenewKeyType.primary.value: + if regenerate_key == RegenerateKeyType.primary.value: pk = generate_key() - if regenerate_key == RenewKeyType.secondary.value: + if regenerate_key == RegenerateKeyType.secondary.value: sk = generate_key() - if regenerate_key == RenewKeyType.swap.value: + if regenerate_key == RegenerateKeyType.swap.value: temp = pk pk = sk sk = temp @@ -1204,7 +1204,6 @@ def iot_hub_configuration_list( result = _iot_hub_configuration_list( cmd, hub_name=hub_name, - top=top, resource_group_name=resource_group_name, login=login, ) @@ -1225,7 +1224,6 @@ def iot_edge_deployment_list( result = _iot_hub_configuration_list( cmd, hub_name=hub_name, - top=top, resource_group_name=resource_group_name, login=login, ) @@ -1235,10 +1233,8 @@ def iot_edge_deployment_list( def _iot_hub_configuration_list( - cmd, hub_name=None, top=None, resource_group_name=None, login=None + cmd, hub_name=None, resource_group_name=None, login=None ): - top = _process_top(top) - discovery = IotHubDiscovery(cmd) target = discovery.get_target( hub_name=hub_name, resource_group_name=resource_group_name, login=login @@ -1247,9 +1243,7 @@ def _iot_hub_configuration_list( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - result = service_sdk.configuration.get_configurations( - top=top, raw=True - ).response.json() + result = service_sdk.configuration.get_configurations(raw=True).response.json() if not result: logger.info('No configurations found on hub "%s".', hub_name) return result diff --git a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py index e681bccaf..d72cdaa98 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py @@ -722,11 +722,7 @@ def test_config_list(self, fixture_cmd, service_client, top): assert len(result) == top or len(result) == service_client.expected_size * 2 list_request = service_client.calls[0].request - - if top: - assert "top={}".format(top) in list_request.url - else: - assert not "top={}".format(top) in list_request.url + assert "top=" not in list_request.url @pytest.mark.parametrize("top", [1, 100, 1000, None]) def test_deployment_list(self, fixture_cmd, service_client, top): @@ -738,11 +734,7 @@ def test_deployment_list(self, fixture_cmd, service_client, top): assert len(result) == top or len(result) == service_client.expected_size list_request = service_client.calls[0].request - - if top: - assert "top={}".format(top) in list_request.url - else: - assert not "top={}".format(top) in list_request.url + assert "top=" not in list_request.url @pytest.mark.parametrize("top", [-1, 0, 101]) def test_config_list_invalid_args(self, fixture_cmd, top): diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index edbf395cd..d85a514c5 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -517,9 +517,9 @@ def test_hub_devices(self): ], ) - # Test 'az iot hub device renew-key' + # Test 'az iot hub device regenerate-key' device = self.cmd( - '''iot hub device-identity renew-key -d {} -n {} -g {} --rk primary + '''iot hub device-identity regenerate-key -d {} -n {} -g {} --kt primary '''.format( edge_device_ids[1], LIVE_HUB, LIVE_RG ), @@ -528,9 +528,9 @@ def test_hub_devices(self): ] ).get_output_in_json() - # Test swap keys 'az iot hub device renew-key' + # Test swap keys 'az iot hub device regenerate-key' self.cmd( - '''iot hub device-identity renew-key -d {} -n {} -g {} --rk swap + '''iot hub device-identity regenerate-key -d {} -n {} -g {} --kt swap '''.format( edge_device_ids[1], LIVE_HUB, LIVE_RG ), @@ -540,8 +540,8 @@ def test_hub_devices(self): ], ) - # Test 'az iot hub device renew-key' with non sas authentication - self.cmd("iot hub device-identity renew-key -d {} -n {} -g {} --rk secondary" + # Test 'az iot hub device regenerate-key' with non sas authentication + self.cmd("iot hub device-identity regenerate-key -d {} -n {} -g {} --kt secondary" .format(device_ids[0], LIVE_HUB, LIVE_RG), expect_failure=True) diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index f18a09560..fdb740ed7 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -661,8 +661,8 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): return service_client @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) - def test_device_key_renew(self, fixture_cmd, serviceclient, req): - subject.iot_device_key_renew( + def test_device_key_regenerate(self, fixture_cmd, serviceclient, req): + subject.iot_device_key_regenerate( fixture_cmd, mock_target["entity"], device_id, req ) args = serviceclient.call_args @@ -699,16 +699,16 @@ def serviceclient_invalid_args(self, mocker, fixture_ghcs, fixture_sas, request) ("swap", CLIError) ] ) - def test_device_key_renew_invalid_args(self, fixture_cmd, serviceclient_invalid_args, req, exp): + def test_device_key_regenerate_invalid_args(self, fixture_cmd, serviceclient_invalid_args, req, exp): with pytest.raises(exp): - subject.iot_device_key_renew( + subject.iot_device_key_regenerate( fixture_cmd, mock_target["entity"], device_id, req ) @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) - def test_device_key_renew_error(self, serviceclient_generic_error, req): + def test_device_key_regenerate_error(self, serviceclient_generic_error, req): with pytest.raises(CLIError): - subject.iot_device_key_renew( + subject.iot_device_key_regenerate( fixture_cmd, mock_target["entity"], device_id, req ) From 0a1ba5c713a56364838a13143712c19ee8309f62 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 8 Oct 2020 15:31:12 -0700 Subject: [PATCH 124/179] Removes PnP model repo assets. Disables PnP runtime commands. (#260) * Removes PnP model repo assets. Disables PnP runtime commands. --- .azure-devops/create-release.yml | 2 +- HISTORY.rst | 13 + azext_iot/__init__.py | 4 - azext_iot/constants.py | 5 +- azext_iot/iothub/_help.py | 2 + azext_iot/iothub/command_map.py | 2 + azext_iot/pnp/__init__.py | 9 - azext_iot/pnp/_help.py | 145 ----- azext_iot/pnp/command_map.py | 39 -- azext_iot/pnp/commands_api.py | 57 -- azext_iot/pnp/commands_repository.py | 56 -- azext_iot/pnp/common.py | 59 -- azext_iot/pnp/params.py | 110 ---- azext_iot/pnp/providers/__init__.py | 65 --- .../pnp/providers/model_repository_api.py | 89 --- azext_iot/pnp/providers/resource.py | 71 --- azext_iot/sdk/pnp/__init__.py | 5 - azext_iot/sdk/pnp/dataplane/__init__.py | 18 - .../digital_twin_model_repository_api.py | 428 --------------- .../sdk/pnp/dataplane/models/__init__.py | 25 - .../pnp/dataplane/models/model_information.py | 112 ---- .../dataplane/models/model_information_py3.py | 112 ---- .../dataplane/models/model_search_options.py | 47 -- .../models/model_search_options_py3.py | 47 -- .../sdk/pnp/dataplane/models/service_error.py | 32 -- .../pnp/dataplane/models/service_error_py3.py | 32 -- azext_iot/sdk/pnp/dataplane/version.py | 13 - azext_iot/sdk/pnp/modelrepository/__init__.py | 18 - .../model_repository_control_plane_api.py | 479 ---------------- .../pnp/modelrepository/models/__init__.py | 28 - .../sdk/pnp/modelrepository/models/subject.py | 40 -- .../models/subject_metadata.py | 32 -- .../models/subject_metadata_py3.py | 32 -- .../pnp/modelrepository/models/subject_py3.py | 40 -- .../sdk/pnp/modelrepository/models/target.py | 32 -- .../pnp/modelrepository/models/target_py3.py | 32 -- .../sdk/pnp/modelrepository/models/tenant.py | 51 -- .../pnp/modelrepository/models/tenant_py3.py | 51 -- azext_iot/sdk/pnp/modelrepository/version.py | 13 - azext_iot/tests/pnp/__init__.py | 15 - azext_iot/tests/pnp/test_iot_pnp_int.py | 303 ----------- azext_iot/tests/pnp/test_iot_pnp_unit.py | 513 ------------------ .../test_pnp_create_payload_interface.json | 201 ------- .../pnp/test_pnp_create_payload_model.json | 13 - .../tests/pnp/test_pnp_interface_show.json | 88 --- setup.py | 1 + 46 files changed, 20 insertions(+), 3561 deletions(-) delete mode 100644 azext_iot/pnp/__init__.py delete mode 100644 azext_iot/pnp/_help.py delete mode 100644 azext_iot/pnp/command_map.py delete mode 100644 azext_iot/pnp/commands_api.py delete mode 100644 azext_iot/pnp/commands_repository.py delete mode 100644 azext_iot/pnp/common.py delete mode 100644 azext_iot/pnp/params.py delete mode 100644 azext_iot/pnp/providers/__init__.py delete mode 100644 azext_iot/pnp/providers/model_repository_api.py delete mode 100644 azext_iot/pnp/providers/resource.py delete mode 100644 azext_iot/sdk/pnp/__init__.py delete mode 100644 azext_iot/sdk/pnp/dataplane/__init__.py delete mode 100644 azext_iot/sdk/pnp/dataplane/digital_twin_model_repository_api.py delete mode 100644 azext_iot/sdk/pnp/dataplane/models/__init__.py delete mode 100644 azext_iot/sdk/pnp/dataplane/models/model_information.py delete mode 100644 azext_iot/sdk/pnp/dataplane/models/model_information_py3.py delete mode 100644 azext_iot/sdk/pnp/dataplane/models/model_search_options.py delete mode 100644 azext_iot/sdk/pnp/dataplane/models/model_search_options_py3.py delete mode 100644 azext_iot/sdk/pnp/dataplane/models/service_error.py delete mode 100644 azext_iot/sdk/pnp/dataplane/models/service_error_py3.py delete mode 100644 azext_iot/sdk/pnp/dataplane/version.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/__init__.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/model_repository_control_plane_api.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/models/__init__.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/models/subject.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/models/subject_metadata.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/models/subject_metadata_py3.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/models/subject_py3.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/models/target.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/models/target_py3.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/models/tenant.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/models/tenant_py3.py delete mode 100644 azext_iot/sdk/pnp/modelrepository/version.py delete mode 100644 azext_iot/tests/pnp/__init__.py delete mode 100644 azext_iot/tests/pnp/test_iot_pnp_int.py delete mode 100644 azext_iot/tests/pnp/test_iot_pnp_unit.py delete mode 100644 azext_iot/tests/pnp/test_pnp_create_payload_interface.json delete mode 100644 azext_iot/tests/pnp/test_pnp_create_payload_model.json delete mode 100644 azext_iot/tests/pnp/test_pnp_interface_show.json diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index ac58ca9d7..be446801a 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -46,7 +46,7 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.6.x' - runUnitTestsOnly: '' + runUnitTestsOnly: 'true' - script: 'pip install .' displayName: 'Install the whl locally' diff --git a/HISTORY.rst b/HISTORY.rst index d9153d3b0..cb899029b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,19 @@ Release History =============== +0.10.3 ++++++++++++++++ + +**General updates** + +* Python 3.5 support will soon be dropped corresponding with the official end of life date. +* Formal python requires constraint added to constrain installs to Py 3.5+. + +**IoT Plug-and-Play updates** + +* The in preview `az iot pnp` command group has been removed. PnP CLI functionality will be re-imagined at a future point in time. + + 0.10.2 +++++++++++++++ diff --git a/azext_iot/__init__.py b/azext_iot/__init__.py index 70ea02e6e..f724bb485 100644 --- a/azext_iot/__init__.py +++ b/azext_iot/__init__.py @@ -28,14 +28,12 @@ def load_command_table(self, args): from azext_iot.iothub.command_map import load_iothub_commands from azext_iot.central.command_map import load_central_commands from azext_iot.digitaltwins.command_map import load_digitaltwins_commands - from azext_iot.pnp.command_map import load_pnp_commands load_command_table(self, args) load_iothub_commands(self, args) load_central_commands(self, args) load_digitaltwins_commands(self, args) load_product_commands(self, args) - load_pnp_commands(self, args) return self.command_table @@ -45,14 +43,12 @@ def load_arguments(self, command): from azext_iot.central.params import load_central_arguments from azext_iot.digitaltwins.params import load_digitaltwins_arguments from azext_iot.product.params import load_product_params - from azext_iot.pnp.params import load_pnp_arguments load_arguments(self, command) load_iothub_arguments(self, command) load_central_arguments(self, command) load_digitaltwins_arguments(self, command) load_product_params(self, command) - load_pnp_arguments(self, command) COMMAND_LOADER_CLS = IoTExtCommandsLoader diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 00ae708b2..39fbb19c4 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.2" +VERSION = "0.10.3" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" @@ -35,9 +35,6 @@ MIN_SIM_MSG_COUNT = 1 SIM_RECEIVE_SLEEP_SEC = 3 CENTRAL_ENDPOINT = "azureiotcentral.com" -PNP_API_VERSION = "2020-05-01-preview" -PNP_ENDPOINT = "azureiotrepository.com" -PNP_TENANT_RESOURCE_ID = "822c8694-ad95-4735-9c55-256f7db2f9b4" DEVICE_DEVICESCOPE_PREFIX = "ms-azure-iot-edge://" TRACING_PROPERTY = "azureiot*com^dtracing^1" TRACING_ALLOWED_FOR_LOCATION = ("northeurope", "westus2", "west us 2", "southeastasia") diff --git a/azext_iot/iothub/_help.py b/azext_iot/iothub/_help.py index 77934f1d4..4249e73a9 100644 --- a/azext_iot/iothub/_help.py +++ b/azext_iot/iothub/_help.py @@ -87,6 +87,7 @@ def load_iothub_help(): az iot hub job cancel --hub-name {iothub_name} --job-id {job_id} """ + ''' helps["iot pnp twin"] = """ type: group short-summary: Manipulate and interact with the digital twin of an IoT Hub device. @@ -140,3 +141,4 @@ def load_iothub_help(): az iot pnp twin update -n {iothub_name} -d {device_id} --json-patch ./my/patch/document.json """ + ''' diff --git a/azext_iot/iothub/command_map.py b/azext_iot/iothub/command_map.py index bc85166b3..c899461da 100644 --- a/azext_iot/iothub/command_map.py +++ b/azext_iot/iothub/command_map.py @@ -25,7 +25,9 @@ def load_iothub_commands(self, _): cmd_group.command("list", "job_list") cmd_group.command("cancel", "job_cancel") + ''' with self.command_group("iot pnp twin", command_type=pnp_runtime_ops) as cmd_group: cmd_group.command("invoke-command", "invoke_device_command") cmd_group.show_command("show", "get_digital_twin") cmd_group.command("update", "patch_digital_twin") + ''' diff --git a/azext_iot/pnp/__init__.py b/azext_iot/pnp/__init__.py deleted file mode 100644 index 8bec87150..000000000 --- a/azext_iot/pnp/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from ._help import load_pnp_help - -load_pnp_help() diff --git a/azext_iot/pnp/_help.py b/azext_iot/pnp/_help.py deleted file mode 100644 index f3f7aac1c..000000000 --- a/azext_iot/pnp/_help.py +++ /dev/null @@ -1,145 +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. -# -------------------------------------------------------------------------------------------- -""" -Help definitions for PnP commands -""" - -from knack.help_files import helps - - -def load_pnp_help(): - - helps["iot pnp"] = """ - type: group - short-summary: Manage Azure IoT Plug-and-Play repositories and models. - """ - - helps["iot pnp repo"] = """ - type: group - short-summary: Create and view Azure IoT Plug-and-Play tenant repositories. - """ - - helps["iot pnp repo create"] = """ - type: command - short-summary: Create a new PnP company repository for your tenant. - long-summary: | - Note that this command takes no parameters. The company repository will be created in your tenant, - and the user who creates the repository will be granted the TenantAdministrator role. - """ - - helps["iot pnp repo list"] = """ - type: command - short-summary: List PnP repositories for your tenant - """ - - helps["iot pnp role-assignment"] = """ - type: group - short-summary: Manage and configure PnP repository and model role assignments. - """ - - helps["iot pnp role-assignment list"] = """ - type: command - short-summary: Lists role assignments for a specific tenant or model. Can be filtered by subject-id. - - examples: - - name: List role assignments for a specific tenant repository - text: > - az iot pnp role-assignment list --resource-id {tenant_id} --resource-type Tenant - - - name: List role assignments for a specific model "dtmi:com:example:ClimateSensor;1" and subject. - text: > - az iot pnp role-assignment list --resource-id "dtmi:com:example:ClimateSensor;1" - --resource-type Model - --subject-id {user_or_spn_id} - """ - - helps["iot pnp role-assignment create"] = """ - type: command - short-summary: Creates a role assignment for a user or service principal to a specific resource. - - examples: - - name: Assign a user the role of Tenant Administrator - text: > - az iot pnp role-assignment create --resource-id {tenant_id} - --resource-type Tenant - --role TenantAdministrator - --subject-id {user_id} - --subject-type User - - - name: Assign a service principal the role of Model Administrator - text: > - az iot pnp role-assignment create --resource-id {tenant_id} - --resource-type Tenant - --role ModelAdministrator - --subject-id {spn_id} - --subject-type ServicePrincipal - """ - - helps["iot pnp role-assignment delete"] = """ - type: command - short-summary: Deletes a role assignment for a user or service principal to a specific resource - - examples: - - name: Remove an assigned role for a specific user - text: > - az iot pnp role-assignment delete --resource-id {tenant_id} - --resource-type Tenant - --role {role} - --subject-id {user_id} - """ - - helps["iot pnp model"] = """ - type: group - short-summary: Create, view, and publish device models in your company repository. - """ - - helps["iot pnp model create"] = """ - type: command - short-summary: Create a new device model in your company repository - - examples: - - name: Create a new model by uploading a JSON file - text: > - az iot pnp model create --model ./path/to/definition/file.json - """ - - helps["iot pnp model show"] = """ - type: command - short-summary: View a device model by ID - - examples: - - name: View a model with the ID "dtmi:com:example:ClimateSensor;1" - text: > - az iot pnp model show --dtmi "dtmi:com:example:ClimateSensor;1" - """ - - helps["iot pnp model list"] = """ - type: command - short-summary: List or search for models in the PnP model repository - - examples: - - name: List all models in the repository - text: > - az iot pnp model list - - - name: Search for all 'Listed' models published by a specific tenant - text: > - az iot pnp model list --state Listed --publisher-id {tenant_id} - - - name: Search for shared interfaces with name or description matching `{keyword}` - text: > - az iot pnp model list -q {keyword} --shared --type Interface - """ - - helps["iot pnp model publish"] = """ - type: command - short-summary: Publish a device model located in your company repository. - - examples: - - name: Publish a model with the ID {dtmi:my:model} - text: > - az iot pnp model publish --model-id {dtmi:my:model} - """ diff --git a/azext_iot/pnp/command_map.py b/azext_iot/pnp/command_map.py deleted file mode 100644 index f3d6a35d2..000000000 --- a/azext_iot/pnp/command_map.py +++ /dev/null @@ -1,39 +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. -# -------------------------------------------------------------------------------------------- - -""" -Load CLI commands -""" -from azure.cli.core.commands import CliCommandType - -pnp_repo_ops = CliCommandType(operations_tmpl="azext_iot.pnp.commands_repository#{}") -pnp_model_ops = CliCommandType(operations_tmpl="azext_iot.pnp.commands_api#{}") - - -def load_pnp_commands(self, _): - """ - Load CLI commands - """ - with self.command_group( - "iot pnp role-assignment", command_type=pnp_repo_ops, is_preview=True - ) as cmd_group: - cmd_group.command("create", "iot_pnp_role_create") - cmd_group.command("list", "iot_pnp_role_list") - cmd_group.command("delete", "iot_pnp_role_delete") - - with self.command_group( - "iot pnp repo", command_type=pnp_repo_ops, is_preview=True - ) as cmd_group: - cmd_group.command("create", "iot_pnp_tenant_create") - cmd_group.command("list", "iot_pnp_tenant_show") - - with self.command_group( - "iot pnp model", command_type=pnp_model_ops, is_preview=True - ) as cmd_group: - cmd_group.show_command("show", "iot_pnp_model_show") - cmd_group.command("create", "iot_pnp_model_create") - cmd_group.command("publish", "iot_pnp_model_publish", confirmation=True) - cmd_group.command("list", "iot_pnp_model_list") diff --git a/azext_iot/pnp/commands_api.py b/azext_iot/pnp/commands_api.py deleted file mode 100644 index fe9c00870..000000000 --- a/azext_iot/pnp/commands_api.py +++ /dev/null @@ -1,57 +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.pnp.providers.model_repository_api import ModelApiProvider -from azext_iot.sdk.pnp.dataplane.models import ModelSearchOptions -from knack.util import CLIError -from azext_iot.common.utility import process_json_arg -from azext_iot.operations.generic import _process_top - - -def iot_pnp_model_show(cmd, model_id, expand=False, pnp_dns_suffix=None): - if not model_id: - raise CLIError("Please provide a model id [--model-id]") - ap = ModelApiProvider(cmd, pnp_dns_suffix) - return ap.get_model(model_id, expand) - - -def iot_pnp_model_list( - cmd, - keyword=None, - model_type=None, - model_state=None, - publisher_id=None, - shared=False, - top=None, - pnp_dns_suffix=None, -): - ap = ModelApiProvider(cmd, pnp_dns_suffix) - search_options = ModelSearchOptions( - search_keyword=keyword, - model_type=model_type, - model_state=model_state, - publisher_id=publisher_id, - ) - - return ap.search_models(search_options, shared, _process_top(top)) - - -def iot_pnp_model_create(cmd, model, pnp_dns_suffix=None): - if not model: - raise CLIError("Please provide a model definition [--model]") - ap = ModelApiProvider(cmd, pnp_dns_suffix) - model = process_json_arg(model, argument_name="model") - model_id = model.get("@id") - if not model_id: - raise CLIError("Model is invalid - @id attribute required.") - return ap.create_model(model_id, model) - - -def iot_pnp_model_publish(cmd, model_id, pnp_dns_suffix=None): - if not model_id: - raise CLIError("Please provide a model id [--model-id]") - ap = ModelApiProvider(cmd, pnp_dns_suffix) - return ap.publish_model(model_id=model_id) diff --git a/azext_iot/pnp/commands_repository.py b/azext_iot/pnp/commands_repository.py deleted file mode 100644 index 637b62b06..000000000 --- a/azext_iot/pnp/commands_repository.py +++ /dev/null @@ -1,56 +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.pnp.providers.resource import RepoResourceProvider -from azext_iot.sdk.pnp.modelrepository.models import Subject - - -def iot_pnp_tenant_create(cmd, pnp_dns_suffix=None): - rp = RepoResourceProvider(cmd, pnp_dns_suffix) - return rp.create() - - -def iot_pnp_tenant_show(cmd, pnp_dns_suffix=None): - rp = RepoResourceProvider(cmd, pnp_dns_suffix) - return rp.list() - - -def iot_pnp_role_create( - cmd, resource_id, resource_type, subject_id, subject_type, role, pnp_dns_suffix=None -): - rp = RepoResourceProvider(cmd, pnp_dns_suffix) - subject = Subject(subject_type=subject_type, role=role, resource_type=resource_type) - return rp.add_role_assignment( - resource_id=resource_id, - subject=subject, - subject_id=subject_id, - resource_type=resource_type, - ) - - -def iot_pnp_role_list( - cmd, resource_id, resource_type, subject_id=None, pnp_dns_suffix=None -): - rp = RepoResourceProvider(cmd, pnp_dns_suffix) - if subject_id: - return rp.get_role_assignments_for_subject( - resource_id=resource_id, subject_id=subject_id, resource_type=resource_type, - ) - return rp.get_role_assignments_for_resource( - resource_id=resource_id, resource_type=resource_type, - ) - - -def iot_pnp_role_delete( - cmd, resource_id, resource_type, role, subject_id, pnp_dns_suffix=None -): - rp = RepoResourceProvider(cmd, pnp_dns_suffix) - return rp.remove_role_assignment( - resource_id=resource_id, - subject_id=subject_id, - resource_type=resource_type, - role_id=role, - ) diff --git a/azext_iot/pnp/common.py b/azext_iot/pnp/common.py deleted file mode 100644 index e3d0f8499..000000000 --- a/azext_iot/pnp/common.py +++ /dev/null @@ -1,59 +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. -# -------------------------------------------------------------------------------------------- - -""" -shared: Define shared data types(enums) -""" - -from enum import Enum - - -class RoleResourceType(Enum): - """ - Type of the Resource - """ - - model = "Model" - tenant = "Tenant" - - -class RoleIdentifier(Enum): - """ - Role Identifier - """ - - modelsPublisher = "ModelsPublisher" - modelsCreator = "ModelsCreator" - tenantAdmin = "TenantAdministrator" - modelAdmin = "ModelAdministrator" - modelReader = "ModelReader" - - -class SubjectType(Enum): - """ - Subject type. - """ - - user = "User" - servicePrincipal = "ServicePrincipal" - - -class ModelType(Enum): - """ - Type of Model - """ - - interface = "Interface" - undetermined = "Undetermined" - - -class ModelState(Enum): - """ - State of a model - """ - - created = "Created" - listed = "Listed" diff --git a/azext_iot/pnp/params.py b/azext_iot/pnp/params.py deleted file mode 100644 index 84ad6b18e..000000000 --- a/azext_iot/pnp/params.py +++ /dev/null @@ -1,110 +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. -# -------------------------------------------------------------------------------------------- - -""" -CLI parameter definitions. -""" - -from azext_iot.pnp.common import ( - RoleResourceType, - RoleIdentifier, - SubjectType, - ModelState, - ModelType, -) -from azure.cli.core.commands.parameters import get_enum_type, get_three_state_flag - - -def load_pnp_arguments(self, _): - """ - Load CLI Args for Knack parser - """ - with self.argument_context("iot pnp") as context: - context.argument( - "pnp_dns_suffix", - options_list=["--pnp-dns-suffix"], - help="An optional PnP DNS suffix used to interact with different PnP environments" - ) - - with self.argument_context("iot pnp role-assignment") as context: - context.argument( - "resource_id", - options_list=["--resource-id"], - help="The ID of the resource to manage role assignments for", - ) - context.argument( - "subject_id", - options_list=["--subject-id"], - help="The ID of a specific subject (User or Service Principal) to manage role assignments for.", - ) - context.argument( - "resource_type", - arg_type=get_enum_type(RoleResourceType), - options_list=["--resource-type"], - help="Resource Type for role", - ) - context.argument( - "role", - arg_type=get_enum_type(RoleIdentifier), - options_list=["--role"], - help="Role for assignment", - ) - with self.argument_context("iot pnp role-assignment create") as context: - context.argument( - "subject_type", - arg_type=get_enum_type(SubjectType), - options_list=["--subject-type"], - help="Subject Type for role assignment", - ) - with self.argument_context("iot pnp model") as context: - context.argument( - "model_id", - options_list=["--model-id", "--dtmi"], - help="Digital Twins model Id. Example: dtmi:com:example:Room;2", - ) - with self.argument_context("iot pnp model create") as context: - context.argument( - "model", - options_list=["--model"], - help="IoT Plug and Play capability-model definition written in DTDL (JSON-LD). " - "Can either be directly input or a file path where the content is extracted from.", - ) - with self.argument_context("iot pnp model show") as context: - context.argument( - "expand", - options_list=["--expand", "--def", "--definition"], - arg_type=get_three_state_flag(), - help="Expand the model’s referenced definitions inline", - ) - with self.argument_context("iot pnp model list") as context: - context.argument( - "keyword", - options_list=["--keyword", "-q"], - help="Restrict model list to those matching a provided keyword", - ) - context.argument( - "model_state", - arg_type=get_enum_type(ModelState), - options_list=["--state", "--model-state"], - help="Restrict model list to models with a specific state", - ) - context.argument( - "model_type", - arg_type=get_enum_type(ModelType), - options_list=["--type", "--model-type"], - help="Restrict model list to models with a specific type", - ) - context.argument( - "publisher_id", - options_list=["--publisher-id", "--pub"], - help="Restrict model list to models published by a specific tenant", - ) - context.argument( - "shared", - arg_type=get_three_state_flag(), - options_list=["--shared"], - help="Restrict model list to shared models only", - ) diff --git a/azext_iot/pnp/providers/__init__.py b/azext_iot/pnp/providers/__init__.py deleted file mode 100644 index 7129949ce..000000000 --- a/azext_iot/pnp/providers/__init__.py +++ /dev/null @@ -1,65 +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.sdk.pnp.modelrepository import ModelRepositoryControlPlaneApi -from azext_iot.sdk.pnp.dataplane import DigitalTwinModelRepositoryApi -from azext_iot.digitaltwins.providers.auth import DigitalTwinAuthentication -from azext_iot.constants import PNP_ENDPOINT, PNP_TENANT_RESOURCE_ID -from msrestazure.azure_exceptions import CloudError - -__all__ = [ - "pnp_modelrepo_service_factory", - "pnp_digitaltwin_modelrepo_api_service_factory", - "PnPModelRepositoryApiManager", - "PnPModelRepositoryManager", - "CloudError", -] - - -def pnp_modelrepo_service_factory(cmd, pnp_dns_suffix=None, *_): - """ - Factory for importing deps and getting service client resources. - Returns: - pnp_modelrepo_resource (ModelRepositoryControlPlaneApi): operational resource for - working with PnP Model Repository Tenants. - """ - creds = DigitalTwinAuthentication(cmd=cmd, resource_id=PNP_TENANT_RESOURCE_ID) - endpoint = "https://provider.{}".format( - pnp_dns_suffix if pnp_dns_suffix else PNP_ENDPOINT - ) - return ModelRepositoryControlPlaneApi(base_url=endpoint, credentials=creds) - - -class PnPModelRepositoryManager(object): - def __init__(self, cmd): - assert cmd - self.cmd = cmd - - def get_mgmt_sdk(self, pnp_dns_suffix=None): - return pnp_modelrepo_service_factory(self.cmd, pnp_dns_suffix) - - -def pnp_digitaltwin_modelrepo_api_service_factory(cmd, pnp_dns_suffix=None, *_): - """ - Factory for importing deps and getting service client resources. - Returns: - pnp_modelrepo_api_resource (DigitalTwinModelRepositoryApi): operational resource for - working with PnP Model Repository data. - """ - creds = DigitalTwinAuthentication(cmd=cmd, resource_id=PNP_TENANT_RESOURCE_ID) - endpoint = "https://repo.{}".format( - pnp_dns_suffix if pnp_dns_suffix else PNP_ENDPOINT - ) - return DigitalTwinModelRepositoryApi(base_url=endpoint, credentials=creds) - - -class PnPModelRepositoryApiManager(object): - def __init__(self, cmd): - assert cmd - self.cmd = cmd - - def get_mgmt_sdk(self, pnp_dns_suffix=None): - return pnp_digitaltwin_modelrepo_api_service_factory(self.cmd, pnp_dns_suffix) diff --git a/azext_iot/pnp/providers/model_repository_api.py b/azext_iot/pnp/providers/model_repository_api.py deleted file mode 100644 index dfa56e1b1..000000000 --- a/azext_iot/pnp/providers/model_repository_api.py +++ /dev/null @@ -1,89 +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.pnp.providers import ( - PnPModelRepositoryApiManager, - CloudError, -) -from azext_iot.pnp.common import ModelState -from azext_iot.common.utility import unpack_msrest_error -from knack.util import CLIError - - -class ModelApiProvider(PnPModelRepositoryApiManager): - def __init__(self, cmd, pnp_dns_suffix=None): - super(ModelApiProvider, self).__init__(cmd=cmd) - self.mgmt_sdk = self.get_mgmt_sdk(pnp_dns_suffix) - - def get_model(self, model_id, expand=False): - try: - return self.mgmt_sdk.get_model_async( - model_id=model_id, expand=expand, raw=True - ).response.json() - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - def create_model( - self, model_id, json_ld_model, - ): - try: - return self.mgmt_sdk.create_or_update_async( - model_id=model_id, - json_ld_model=json_ld_model, - x_ms_model_state=ModelState.created.value, - raw=True, - ).response.json() - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - def publish_model( - self, model_id, - ): - try: - model_response = self.mgmt_sdk.get_model_async(model_id, raw=True) - etag = model_response.response.headers.get("eTag") - if not etag: - raise CLIError( - "No model found with @id `{}` to publish".format(model_id) - ) - etag = etag.replace('\\"', "") - return self.mgmt_sdk.create_or_update_async( - model_id=model_id, - update_metadata=True, - if_match=etag, - x_ms_model_state=ModelState.listed.value, - raw=True, - ).response.json() - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - def search_models( - self, search_options, shared_models_only=None, top=None, - ): - try: - payload = [] - headers = {"Cache-Control": "no-cache, must-revalidate"} - - result = self.mgmt_sdk.search_models_async( - model_search_options=search_options, - x_ms_show_shared_models_only=shared_models_only, - custom_headers=headers, - raw=True, - ) - - payload.extend(result.response.json()) - - return payload[:top] if top else payload - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - def validate_models(self, models=None, validate_dependencies=None): - try: - return self.mgmt_sdk.are_valid_models( - json_ld_models=models, validate_dependencies=validate_dependencies, - ) - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/pnp/providers/resource.py b/azext_iot/pnp/providers/resource.py deleted file mode 100644 index bb42a7865..000000000 --- a/azext_iot/pnp/providers/resource.py +++ /dev/null @@ -1,71 +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.pnp.providers import ( - PnPModelRepositoryManager, - CloudError, -) -from azext_iot.common.utility import unpack_msrest_error -from knack.util import CLIError - - -class RepoResourceProvider(PnPModelRepositoryManager): - def __init__(self, cmd, pnp_dns_suffix=None): - super(RepoResourceProvider, self).__init__(cmd=cmd) - self.mgmt_sdk = self.get_mgmt_sdk(pnp_dns_suffix) - - def create(self): - try: - return self.mgmt_sdk.create_tenant_async(self) - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - def list(self): - try: - return self.mgmt_sdk.get_tenant_async(self) - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - # RBAC - def get_role_assignments_for_resource(self, resource_id, resource_type): - try: - return self.mgmt_sdk.get_subjects_for_resources_async( - resource_id=resource_id, resource_type=resource_type, - ) - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - def get_role_assignments_for_subject(self, resource_id, resource_type, subject_id): - try: - return self.mgmt_sdk.get_subjects_for_resources_async1( - resource_id=resource_id, - resource_type=resource_type, - subject_id=subject_id, - ) - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - def add_role_assignment(self, resource_id, resource_type, subject_id, subject=None): - try: - return self.mgmt_sdk.assign_roles_async( - resource_id=resource_id, - resource_type=resource_type, - subject=subject, - subject_id=subject_id, - ) - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) - - def remove_role_assignment(self, resource_id, resource_type, role_id, subject_id): - try: - return self.mgmt_sdk.remove_roles_async( - resource_id=resource_id, - resource_type=resource_type, - subject_id=subject_id, - role_id=role_id, - ) - except CloudError as e: - raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/sdk/pnp/__init__.py b/azext_iot/sdk/pnp/__init__.py deleted file mode 100644 index 55614acbf..000000000 --- a/azext_iot/sdk/pnp/__init__.py +++ /dev/null @@ -1,5 +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. -# -------------------------------------------------------------------------------------------- diff --git a/azext_iot/sdk/pnp/dataplane/__init__.py b/azext_iot/sdk/pnp/dataplane/__init__.py deleted file mode 100644 index 918faacf0..000000000 --- a/azext_iot/sdk/pnp/dataplane/__init__.py +++ /dev/null @@ -1,18 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .digital_twin_model_repository_api import DigitalTwinModelRepositoryApi -from .version import VERSION - -__all__ = ['DigitalTwinModelRepositoryApi'] - -__version__ = VERSION - diff --git a/azext_iot/sdk/pnp/dataplane/digital_twin_model_repository_api.py b/azext_iot/sdk/pnp/dataplane/digital_twin_model_repository_api.py deleted file mode 100644 index bca99949b..000000000 --- a/azext_iot/sdk/pnp/dataplane/digital_twin_model_repository_api.py +++ /dev/null @@ -1,428 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.service_client import SDKClient -from msrest import Serializer, Deserializer -from msrestazure import AzureConfiguration -from .version import VERSION -from msrest.pipeline import ClientRawResponse -from msrestazure.azure_exceptions import CloudError -import uuid -from . import models -from azext_iot.constants import USER_AGENT - - -class DigitalTwinModelRepositoryApiConfiguration(AzureConfiguration): - """Configuration for DigitalTwinModelRepositoryApi - Note that all parameters used to create this instance are saved as instance - attributes. - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - if credentials is None: - raise ValueError("Parameter 'credentials' must not be None.") - if not base_url: - base_url = 'http://localhost' - - super(DigitalTwinModelRepositoryApiConfiguration, self).__init__(base_url) - - self.add_user_agent('digitaltwinmodelrepositoryapi/{}'.format(VERSION)) - self.add_user_agent(USER_AGENT) # @c-ryan-k - - self.credentials = credentials - - -class DigitalTwinModelRepositoryApi(SDKClient): - """DigitalTwinModelRepositoryApi - - :ivar config: Configuration for client. - :vartype config: DigitalTwinModelRepositoryApiConfiguration - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - self.config = DigitalTwinModelRepositoryApiConfiguration(credentials, base_url) - super(DigitalTwinModelRepositoryApi, self).__init__(self.config.credentials, self.config) - - client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2020-05-01-preview' - self._serialize = Serializer(client_models) - self._deserialize = Deserializer(client_models) - - - def get_model_async( - self, model_id, expand=None, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Gets a Digital Twin model definition for the given digital twin model - Id. - - :param model_id: Digital Twin model Id e.g.: - dtmi:com:contoso:temperaturesensor;1. - :type model_id: str - :param expand: Gets or sets a value indicating whether indicates - whether to expand the model�s referenced definitions inline or not. - :type expand: bool - :param x_ms_client_request_id: Gets or sets optional. Provides a - client-generated value that is recorded in the logs. Using this header - is highly recommended for correlating client-side activities with - requests received by the server. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.get_model_async.metadata['url'] - path_format_arguments = { - 'modelId': self._serialize.url("model_id", model_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - if expand is not None: - query_parameters['expand'] = self._serialize.query("expand", expand, 'bool') - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/ld+json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200, 401, 404, 500]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('str', response) - if response.status_code == 401: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 404: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 500: - deserialized = self._deserialize('ServiceError', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_model_async.metadata = {'url': '/models/{modelId}'} - - def create_or_update_async( - self, model_id, json_ld_model=None, if_match=None, update_metadata=None, x_ms_model_state=None, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Creates a Digital Twin model or updates meta data properties of a - Digital Twin model. - - :param model_id: Digital Twin model Id e.g.: - dtmi:com:contoso:temperaturesensor;1. - :type model_id: str - :param json_ld_model: Model definition in Digital Twin Definition - Language (DTDL) format. - :type json_ld_model: object - :param if_match: Gets or sets model definition in Digital Twin - Definition Language (DTDL) format. - :type if_match: str - :param update_metadata: Gets or sets a value indicating whether used - to modify metadata properties supplied in the Header. - :type update_metadata: bool - :param x_ms_model_state: Gets or sets model Lifecycle state. Possible - values include: 'Created', 'Listed' - :type x_ms_model_state: str - :param x_ms_client_request_id: Gets or sets optional. Provides a - client-generated value that is recorded in the logs. Using this header - is highly recommended for correlating client-side activities with - requests received by the server. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.create_or_update_async.metadata['url'] - path_format_arguments = { - 'modelId': self._serialize.url("model_id", model_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - if update_metadata is not None: - query_parameters['update-metadata'] = self._serialize.query("update_metadata", update_metadata, 'bool') - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['if-match'] = self._serialize.header("if_match", if_match, 'str') - if x_ms_model_state is not None: - header_parameters['x-ms-model-state'] = self._serialize.header("x_ms_model_state", x_ms_model_state, 'str') - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - if json_ld_model is not None: - body_content = self._serialize.body(json_ld_model, 'object') - else: - body_content = None - - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200, 201, 401, 404, 409, 412, 415, 428, 500]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('str', response) - if response.status_code == 201: - deserialized = self._deserialize('str', response) - if response.status_code == 401: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 404: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 409: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 412: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 415: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 428: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 500: - deserialized = self._deserialize('ServiceError', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_or_update_async.metadata = {'url': '/models/{modelId}'} - - def search_models_async( - self, model_search_options=None, x_ms_page_size=None, x_ms_show_shared_models_only=None, x_ms_continuation_token=None, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Queries Model repository for Digital Twin models matching search - options. - - :param model_search_options: Gets or sets model Search Options. - :type model_search_options: ~pnp.models.ModelSearchOptions - :param x_ms_page_size: Gets or sets when specified, it ensures that - paged results are returned. If not specified, default value is 50. - :type x_ms_page_size: int - :param x_ms_show_shared_models_only: Gets or sets a value indicating - whether [show shared models]. - :type x_ms_show_shared_models_only: bool - :param x_ms_continuation_token: Gets or sets when there are more - results than a page size, server responds with a continuation token. - Supply this token to retrieve next set of page results. - :type x_ms_continuation_token: str - :param x_ms_client_request_id: Gets or sets optional. Provides a - client-generated value that is recorded in the logs. Using this header - is highly recommended for correlating client-side activities with - requests received by the server. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.search_models_async.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/ld+json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_page_size is not None: - header_parameters['x-ms-page-size'] = self._serialize.header("x_ms_page_size", x_ms_page_size, 'int') - if x_ms_show_shared_models_only is not None: - header_parameters['x-ms-show-shared-models-only'] = self._serialize.header("x_ms_show_shared_models_only", x_ms_show_shared_models_only, 'bool') - if x_ms_continuation_token is not None: - header_parameters['x-ms-continuation-token'] = self._serialize.header("x_ms_continuation_token", x_ms_continuation_token, 'str') - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - if model_search_options is not None: - body_content = self._serialize.body(model_search_options, 'ModelSearchOptions') - else: - body_content = None - - # Construct and send request - request = self._client.post(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200, 404, 500]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[ModelInformation]', response) - if response.status_code == 404: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 500: - deserialized = self._deserialize('ServiceError', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - search_models_async.metadata = {'url': '/models/search'} - - def are_valid_models( - self, json_ld_models=None, x_ms_client_request_id=None, validate_dependencies=None, custom_headers=None, raw=False, **operation_config): - """Validates the models according to the DTDL spec. - - :param json_ld_models: The json ld models. - :type json_ld_models: list[object] - :param x_ms_client_request_id: Gets or sets optional. Provides a - client-generated value that is recorded in the logs. Using this header - is highly recommended for correlating client-side activities with - requests received by the server. - :type x_ms_client_request_id: str - :param validate_dependencies: Gets or sets a value indicating whether - [validate dependencies]. - :type validate_dependencies: bool - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.are_valid_models.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - if validate_dependencies is not None: - query_parameters['validateDependencies'] = self._serialize.query("validate_dependencies", validate_dependencies, 'bool') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - if json_ld_models is not None: - body_content = self._serialize.body(json_ld_models, '[object]') - else: - body_content = None - - # Construct and send request - request = self._client.post(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200, 400, 500]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('str', response) - if response.status_code == 400: - deserialized = self._deserialize('ServiceError', response) - if response.status_code == 500: - deserialized = self._deserialize('ServiceError', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - are_valid_models.metadata = {'url': '/models/validate'} diff --git a/azext_iot/sdk/pnp/dataplane/models/__init__.py b/azext_iot/sdk/pnp/dataplane/models/__init__.py deleted file mode 100644 index 004c583bd..000000000 --- a/azext_iot/sdk/pnp/dataplane/models/__init__.py +++ /dev/null @@ -1,25 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -try: - from .model_information_py3 import ModelInformation - from .service_error_py3 import ServiceError - from .model_search_options_py3 import ModelSearchOptions -except (SyntaxError, ImportError): - from .model_information import ModelInformation - from .service_error import ServiceError - from .model_search_options import ModelSearchOptions - -__all__ = [ - 'ModelInformation', - 'ServiceError', - 'ModelSearchOptions', -] diff --git a/azext_iot/sdk/pnp/dataplane/models/model_information.py b/azext_iot/sdk/pnp/dataplane/models/model_information.py deleted file mode 100644 index c18382557..000000000 --- a/azext_iot/sdk/pnp/dataplane/models/model_information.py +++ /dev/null @@ -1,112 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ModelInformation(Model): - """Model Metadata. - - :param comment: Gets or sets comments value in the model. - :type comment: str - :param description: Gets or sets description about the model. - :type description: str - :param display_name: Gets or sets display Name of the model. - :type display_name: str - :param model_id: Gets or sets model Identifier. - :type model_id: str - :param model_name: Gets or sets model Name. - :type model_name: str - :param version: Gets or sets modelVersion of the model. - :type version: str - :param model_type: Gets or sets model Definition Type. Possible values - include: 'Interface', 'Undetermined' - :type model_type: str or ~pnp.models.enum - :param publisher_id: Gets or sets aad Tenant Id of the model publisher. - :type publisher_id: str - :param publisher_name: Gets or sets aad Tenant Name of the model - publisher. - :type publisher_name: str - :param created_by: Gets or sets the identity of the user who created the - model. - :type created_by: str - :param created_date: Gets or sets created Date and Time of the model. - :type created_date: datetime - :param updated_date: Gets or sets updated date and time of the model. - :type updated_date: datetime - :param model_state: Gets or sets the state of the model. - Created or - Listed. Possible values include: 'Created', 'Listed' - :type model_state: str or ~pnp.models.enum - :param _etag: Gets or sets the e tag. - :type _etag: str - :param count_of_components: Gets or sets the number of components. - :type count_of_components: int - :param count_of_commands: Gets or sets the number of commands. - :type count_of_commands: int - :param count_of_telemetries: Gets or sets the number of telemetries. - :type count_of_telemetries: int - :param count_of_properties: Gets or sets the number of properties. - :type count_of_properties: int - :param count_of_relationships: Gets or sets the number of relationships. - :type count_of_relationships: int - :param count_of_extends: Gets or sets the number of extends. - :type count_of_extends: int - :param count_of_schemas: Gets or sets the number of schemas. - :type count_of_schemas: int - """ - - _attribute_map = { - 'comment': {'key': 'comment', 'type': 'str'}, - 'description': {'key': 'description', 'type': 'str'}, - 'display_name': {'key': 'displayName', 'type': 'str'}, - 'model_id': {'key': 'modelId', 'type': 'str'}, - 'model_name': {'key': 'modelName', 'type': 'str'}, - 'version': {'key': 'version', 'type': 'str'}, - 'model_type': {'key': 'modelType', 'type': 'str'}, - 'publisher_id': {'key': 'publisherId', 'type': 'str'}, - 'publisher_name': {'key': 'publisherName', 'type': 'str'}, - 'created_by': {'key': 'createdBy', 'type': 'str'}, - 'created_date': {'key': 'createdDate', 'type': 'iso-8601'}, - 'updated_date': {'key': 'updatedDate', 'type': 'iso-8601'}, - 'model_state': {'key': 'modelState', 'type': 'str'}, - '_etag': {'key': '_etag', 'type': 'str'}, - 'count_of_components': {'key': 'countOfComponents', 'type': 'int'}, - 'count_of_commands': {'key': 'countOfCommands', 'type': 'int'}, - 'count_of_telemetries': {'key': 'countOfTelemetries', 'type': 'int'}, - 'count_of_properties': {'key': 'countOfProperties', 'type': 'int'}, - 'count_of_relationships': {'key': 'countOfRelationships', 'type': 'int'}, - 'count_of_extends': {'key': 'countOfExtends', 'type': 'int'}, - 'count_of_schemas': {'key': 'countOfSchemas', 'type': 'int'}, - } - - def __init__(self, **kwargs): - super(ModelInformation, self).__init__(**kwargs) - self.comment = kwargs.get('comment', None) - self.description = kwargs.get('description', None) - self.display_name = kwargs.get('display_name', None) - self.model_id = kwargs.get('model_id', None) - self.model_name = kwargs.get('model_name', None) - self.version = kwargs.get('version', None) - self.model_type = kwargs.get('model_type', None) - self.publisher_id = kwargs.get('publisher_id', None) - self.publisher_name = kwargs.get('publisher_name', None) - self.created_by = kwargs.get('created_by', None) - self.created_date = kwargs.get('created_date', None) - self.updated_date = kwargs.get('updated_date', None) - self.model_state = kwargs.get('model_state', None) - self._etag = kwargs.get('_etag', None) - self.count_of_components = kwargs.get('count_of_components', None) - self.count_of_commands = kwargs.get('count_of_commands', None) - self.count_of_telemetries = kwargs.get('count_of_telemetries', None) - self.count_of_properties = kwargs.get('count_of_properties', None) - self.count_of_relationships = kwargs.get('count_of_relationships', None) - self.count_of_extends = kwargs.get('count_of_extends', None) - self.count_of_schemas = kwargs.get('count_of_schemas', None) diff --git a/azext_iot/sdk/pnp/dataplane/models/model_information_py3.py b/azext_iot/sdk/pnp/dataplane/models/model_information_py3.py deleted file mode 100644 index 225d563cc..000000000 --- a/azext_iot/sdk/pnp/dataplane/models/model_information_py3.py +++ /dev/null @@ -1,112 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ModelInformation(Model): - """Model Metadata. - - :param comment: Gets or sets comments value in the model. - :type comment: str - :param description: Gets or sets description about the model. - :type description: str - :param display_name: Gets or sets display Name of the model. - :type display_name: str - :param model_id: Gets or sets model Identifier. - :type model_id: str - :param model_name: Gets or sets model Name. - :type model_name: str - :param version: Gets or sets modelVersion of the model. - :type version: str - :param model_type: Gets or sets model Definition Type. Possible values - include: 'Interface', 'Undetermined' - :type model_type: str or ~pnp.models.enum - :param publisher_id: Gets or sets aad Tenant Id of the model publisher. - :type publisher_id: str - :param publisher_name: Gets or sets aad Tenant Name of the model - publisher. - :type publisher_name: str - :param created_by: Gets or sets the identity of the user who created the - model. - :type created_by: str - :param created_date: Gets or sets created Date and Time of the model. - :type created_date: datetime - :param updated_date: Gets or sets updated date and time of the model. - :type updated_date: datetime - :param model_state: Gets or sets the state of the model. - Created or - Listed. Possible values include: 'Created', 'Listed' - :type model_state: str or ~pnp.models.enum - :param _etag: Gets or sets the e tag. - :type _etag: str - :param count_of_components: Gets or sets the number of components. - :type count_of_components: int - :param count_of_commands: Gets or sets the number of commands. - :type count_of_commands: int - :param count_of_telemetries: Gets or sets the number of telemetries. - :type count_of_telemetries: int - :param count_of_properties: Gets or sets the number of properties. - :type count_of_properties: int - :param count_of_relationships: Gets or sets the number of relationships. - :type count_of_relationships: int - :param count_of_extends: Gets or sets the number of extends. - :type count_of_extends: int - :param count_of_schemas: Gets or sets the number of schemas. - :type count_of_schemas: int - """ - - _attribute_map = { - 'comment': {'key': 'comment', 'type': 'str'}, - 'description': {'key': 'description', 'type': 'str'}, - 'display_name': {'key': 'displayName', 'type': 'str'}, - 'model_id': {'key': 'modelId', 'type': 'str'}, - 'model_name': {'key': 'modelName', 'type': 'str'}, - 'version': {'key': 'version', 'type': 'str'}, - 'model_type': {'key': 'modelType', 'type': 'str'}, - 'publisher_id': {'key': 'publisherId', 'type': 'str'}, - 'publisher_name': {'key': 'publisherName', 'type': 'str'}, - 'created_by': {'key': 'createdBy', 'type': 'str'}, - 'created_date': {'key': 'createdDate', 'type': 'iso-8601'}, - 'updated_date': {'key': 'updatedDate', 'type': 'iso-8601'}, - 'model_state': {'key': 'modelState', 'type': 'str'}, - '_etag': {'key': '_etag', 'type': 'str'}, - 'count_of_components': {'key': 'countOfComponents', 'type': 'int'}, - 'count_of_commands': {'key': 'countOfCommands', 'type': 'int'}, - 'count_of_telemetries': {'key': 'countOfTelemetries', 'type': 'int'}, - 'count_of_properties': {'key': 'countOfProperties', 'type': 'int'}, - 'count_of_relationships': {'key': 'countOfRelationships', 'type': 'int'}, - 'count_of_extends': {'key': 'countOfExtends', 'type': 'int'}, - 'count_of_schemas': {'key': 'countOfSchemas', 'type': 'int'}, - } - - def __init__(self, *, comment: str=None, description: str=None, display_name: str=None, model_id: str=None, model_name: str=None, version: str=None, model_type=None, publisher_id: str=None, publisher_name: str=None, created_by: str=None, created_date=None, updated_date=None, model_state=None, _etag: str=None, count_of_components: int=None, count_of_commands: int=None, count_of_telemetries: int=None, count_of_properties: int=None, count_of_relationships: int=None, count_of_extends: int=None, count_of_schemas: int=None, **kwargs) -> None: - super(ModelInformation, self).__init__(**kwargs) - self.comment = comment - self.description = description - self.display_name = display_name - self.model_id = model_id - self.model_name = model_name - self.version = version - self.model_type = model_type - self.publisher_id = publisher_id - self.publisher_name = publisher_name - self.created_by = created_by - self.created_date = created_date - self.updated_date = updated_date - self.model_state = model_state - self._etag = _etag - self.count_of_components = count_of_components - self.count_of_commands = count_of_commands - self.count_of_telemetries = count_of_telemetries - self.count_of_properties = count_of_properties - self.count_of_relationships = count_of_relationships - self.count_of_extends = count_of_extends - self.count_of_schemas = count_of_schemas diff --git a/azext_iot/sdk/pnp/dataplane/models/model_search_options.py b/azext_iot/sdk/pnp/dataplane/models/model_search_options.py deleted file mode 100644 index 4e7180c8a..000000000 --- a/azext_iot/sdk/pnp/dataplane/models/model_search_options.py +++ /dev/null @@ -1,47 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ModelSearchOptions(Model): - """Model Search Options. - - :param search_keyword: Gets or sets keyword for model search. Searches - within pre-defined properties. - :type search_keyword: str - :param model_type: Gets or sets the type of the model. Possible values - include: 'Interface', 'Undetermined' - :type model_type: str or ~pnp.models.enum - :param model_state: Gets or sets the state of the model. Possible values - include: 'Created', 'Listed' - :type model_state: str or ~pnp.models.enum - :param publisher_id: Gets or sets the publisher identifier. - :type publisher_id: str - :param created_by: Gets or sets the created by. - :type created_by: str - """ - - _attribute_map = { - 'search_keyword': {'key': 'searchKeyword', 'type': 'str'}, - 'model_type': {'key': 'modelType', 'type': 'str'}, - 'model_state': {'key': 'modelState', 'type': 'str'}, - 'publisher_id': {'key': 'publisherId', 'type': 'str'}, - 'created_by': {'key': 'createdBy', 'type': 'str'}, - } - - def __init__(self, **kwargs): - super(ModelSearchOptions, self).__init__(**kwargs) - self.search_keyword = kwargs.get('search_keyword', None) - self.model_type = kwargs.get('model_type', None) - self.model_state = kwargs.get('model_state', None) - self.publisher_id = kwargs.get('publisher_id', None) - self.created_by = kwargs.get('created_by', None) diff --git a/azext_iot/sdk/pnp/dataplane/models/model_search_options_py3.py b/azext_iot/sdk/pnp/dataplane/models/model_search_options_py3.py deleted file mode 100644 index d32952302..000000000 --- a/azext_iot/sdk/pnp/dataplane/models/model_search_options_py3.py +++ /dev/null @@ -1,47 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ModelSearchOptions(Model): - """Model Search Options. - - :param search_keyword: Gets or sets keyword for model search. Searches - within pre-defined properties. - :type search_keyword: str - :param model_type: Gets or sets the type of the model. Possible values - include: 'Interface', 'Undetermined' - :type model_type: str or ~pnp.models.enum - :param model_state: Gets or sets the state of the model. Possible values - include: 'Created', 'Listed' - :type model_state: str or ~pnp.models.enum - :param publisher_id: Gets or sets the publisher identifier. - :type publisher_id: str - :param created_by: Gets or sets the created by. - :type created_by: str - """ - - _attribute_map = { - 'search_keyword': {'key': 'searchKeyword', 'type': 'str'}, - 'model_type': {'key': 'modelType', 'type': 'str'}, - 'model_state': {'key': 'modelState', 'type': 'str'}, - 'publisher_id': {'key': 'publisherId', 'type': 'str'}, - 'created_by': {'key': 'createdBy', 'type': 'str'}, - } - - def __init__(self, *, search_keyword: str=None, model_type=None, model_state=None, publisher_id: str=None, created_by: str=None, **kwargs) -> None: - super(ModelSearchOptions, self).__init__(**kwargs) - self.search_keyword = search_keyword - self.model_type = model_type - self.model_state = model_state - self.publisher_id = publisher_id - self.created_by = created_by diff --git a/azext_iot/sdk/pnp/dataplane/models/service_error.py b/azext_iot/sdk/pnp/dataplane/models/service_error.py deleted file mode 100644 index 1e66b591d..000000000 --- a/azext_iot/sdk/pnp/dataplane/models/service_error.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ServiceError(Model): - """Error Class. - - :param code: Gets or sets the code. - :type code: str - :param message: Gets or sets the message. - :type message: str - """ - - _attribute_map = { - 'code': {'key': 'code', 'type': 'str'}, - 'message': {'key': 'message', 'type': 'str'}, - } - - def __init__(self, **kwargs): - super(ServiceError, self).__init__(**kwargs) - self.code = kwargs.get('code', None) - self.message = kwargs.get('message', None) diff --git a/azext_iot/sdk/pnp/dataplane/models/service_error_py3.py b/azext_iot/sdk/pnp/dataplane/models/service_error_py3.py deleted file mode 100644 index f7bae1062..000000000 --- a/azext_iot/sdk/pnp/dataplane/models/service_error_py3.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class ServiceError(Model): - """Error Class. - - :param code: Gets or sets the code. - :type code: str - :param message: Gets or sets the message. - :type message: str - """ - - _attribute_map = { - 'code': {'key': 'code', 'type': 'str'}, - 'message': {'key': 'message', 'type': 'str'}, - } - - def __init__(self, *, code: str=None, message: str=None, **kwargs) -> None: - super(ServiceError, self).__init__(**kwargs) - self.code = code - self.message = message diff --git a/azext_iot/sdk/pnp/dataplane/version.py b/azext_iot/sdk/pnp/dataplane/version.py deleted file mode 100644 index fc729cd31..000000000 --- a/azext_iot/sdk/pnp/dataplane/version.py +++ /dev/null @@ -1,13 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -VERSION = "2020-05-01-preview" - diff --git a/azext_iot/sdk/pnp/modelrepository/__init__.py b/azext_iot/sdk/pnp/modelrepository/__init__.py deleted file mode 100644 index 9902856bd..000000000 --- a/azext_iot/sdk/pnp/modelrepository/__init__.py +++ /dev/null @@ -1,18 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .model_repository_control_plane_api import ModelRepositoryControlPlaneApi -from .version import VERSION - -__all__ = ['ModelRepositoryControlPlaneApi'] - -__version__ = VERSION - diff --git a/azext_iot/sdk/pnp/modelrepository/model_repository_control_plane_api.py b/azext_iot/sdk/pnp/modelrepository/model_repository_control_plane_api.py deleted file mode 100644 index e9c73ae4b..000000000 --- a/azext_iot/sdk/pnp/modelrepository/model_repository_control_plane_api.py +++ /dev/null @@ -1,479 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.service_client import SDKClient -from msrest import Serializer, Deserializer -from msrestazure import AzureConfiguration -from .version import VERSION -from msrest.pipeline import ClientRawResponse -from msrestazure.azure_exceptions import CloudError -import uuid -from . import models -from azext_iot.constants import USER_AGENT - -class ModelRepositoryControlPlaneApiConfiguration(AzureConfiguration): - """Configuration for ModelRepositoryControlPlaneApi - Note that all parameters used to create this instance are saved as instance - attributes. - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - if credentials is None: - raise ValueError("Parameter 'credentials' must not be None.") - if not base_url: - base_url = 'http://localhost' - - super(ModelRepositoryControlPlaneApiConfiguration, self).__init__(base_url) - - self.add_user_agent('modelrepositorycontrolplaneapi/{}'.format(VERSION)) - self.add_user_agent(USER_AGENT) # @c-ryan-k - - self.credentials = credentials - - -class ModelRepositoryControlPlaneApi(SDKClient): - """ModelRepositoryControlPlaneApi - - :ivar config: Configuration for client. - :vartype config: ModelRepositoryControlPlaneApiConfiguration - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - self.config = ModelRepositoryControlPlaneApiConfiguration(credentials, base_url) - super(ModelRepositoryControlPlaneApi, self).__init__(self.config.credentials, self.config) - - client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2020-05-01-preview' - self._serialize = Serializer(client_models) - self._deserialize = Deserializer(client_models) - - - def get_subjects_for_resources_async( - self, resource_id, resource_type, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Get the access permission for resource. - - :param resource_id: The resource identifier. - :type resource_id: str - :param resource_type: The resource Type. Possible values include: - 'Model', 'Tenant' - :type resource_type: str - :param x_ms_client_request_id: Gets or sets optional. Provides a - client-generated value that is recorded in the logs. Using this header - is highly recommended for correlating client-side activities with - requests received by the server. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~pnp.models.Target] or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.get_subjects_for_resources_async.metadata['url'] - path_format_arguments = { - 'resourceId': self._serialize.url("resource_id", resource_id, 'str'), - 'resourceType': self._serialize.url("resource_type", resource_type, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[Target]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_subjects_for_resources_async.metadata = {'url': '/resources/{resourceId}/types/{resourceType}/targets'} - - def get_subjects_for_resources_async1( - self, resource_id, subject_id, resource_type, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Get the access permission for resource for the specified - subject/principal. - - :param resource_id: The resource Id. - :type resource_id: str - :param subject_id: The Subject Id. - :type subject_id: str - :param resource_type: The resource Type. Possible values include: - 'Model', 'Tenant' - :type resource_type: str - :param x_ms_client_request_id: Gets or sets optional. Provides a - client-generated value that is recorded in the logs. Using this header - is highly recommended for correlating client-side activities with - requests received by the server. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~pnp.models.Target] or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.get_subjects_for_resources_async1.metadata['url'] - path_format_arguments = { - 'resourceId': self._serialize.url("resource_id", resource_id, 'str'), - 'subjectId': self._serialize.url("subject_id", subject_id, 'str'), - 'resourceType': self._serialize.url("resource_type", resource_type, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[Target]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_subjects_for_resources_async1.metadata = {'url': '/resources/{resourceId}/types/{resourceType}/subjects/{subjectId}/targets'} - - def assign_roles_async( - self, resource_id, resource_type, subject_id, x_ms_client_request_id=None, subject=None, custom_headers=None, raw=False, **operation_config): - """Assign roles and permissions for a subject/principal to a resource. - - :param resource_id: The resource identifier. - :type resource_id: str - :param resource_type: The resource type. Possible values include: - 'Model', 'Tenant' - :type resource_type: str - :param subject_id: The subject identifier. - :type subject_id: str - :param x_ms_client_request_id: Gets or sets optional clients request. - Provides a client-generated value that is recorded in the logs. Using - this header is highly recommended for correlating client-side - activities with requests received by the server. - :type x_ms_client_request_id: str - :param subject: Gets or sets the subject. - :type subject: ~pnp.models.Subject - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.assign_roles_async.metadata['url'] - path_format_arguments = { - 'resourceId': self._serialize.url("resource_id", resource_id, 'str'), - 'resourceType': self._serialize.url("resource_type", resource_type, 'str'), - 'subjectId': self._serialize.url("subject_id", subject_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json-patch+json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - if subject is not None: - body_content = self._serialize.body(subject, 'Subject') - else: - body_content = None - - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - assign_roles_async.metadata = {'url': '/resources/{resourceId}/types/{resourceType}/subjects/{subjectId}'} - - def remove_roles_async( - self, resource_id, subject_id, resource_type, role_id, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Removes the roles assigned to a subject/principal. - - :param resource_id: The resource identifier. - :type resource_id: str - :param subject_id: The subject identifier. - :type subject_id: str - :param resource_type: The resource type. Possible values include: - 'Model', 'Tenant' - :type resource_type: str - :param role_id: The roleIdentifier. Possible values include: - 'ModelsPublisher', 'ModelsCreator', 'TenantAdministrator', - 'ModelAdministrator', 'ModelReader', 'None' - :type role_id: str - :param x_ms_client_request_id: Gets or sets optional. Provides a - client-generated value that is recorded in the logs. Using this header - is highly recommended for correlating client-side activities with - requests received by the server. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.remove_roles_async.metadata['url'] - path_format_arguments = { - 'resourceId': self._serialize.url("resource_id", resource_id, 'str'), - 'subjectId': self._serialize.url("subject_id", subject_id, 'str'), - 'resourceType': self._serialize.url("resource_type", resource_type, 'str'), - 'roleId': self._serialize.url("role_id", role_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - remove_roles_async.metadata = {'url': '/resources/{resourceId}/types/{resourceType}/subjects/{subjectId}/roles/{roleId}'} - - def get_tenant_async( - self, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Gets the information about the tenant. - - :param x_ms_client_request_id: Gets or sets optional. Provides a - client-generated value that is recorded in the logs. Using this header - is highly recommended for correlating client-side activities with - requests received by the server. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~pnp.models.Tenant] or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.get_tenant_async.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[Tenant]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_tenant_async.metadata = {'url': '/tenants'} - - def create_tenant_async( - self, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config): - """Creates a new tenant. - - :param x_ms_client_request_id: Gets or sets optional. Provides a - client-generated value that is recorded in the logs. Using this header - is highly recommended for correlating client-side activities with - requests received by the server. - :type x_ms_client_request_id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Tenant or ClientRawResponse if raw=true - :rtype: ~pnp.models.Tenant or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - api_version = "2020-05-01-preview" - - # Construct URL - url = self.create_tenant_async.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_client_request_id is not None: - header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Tenant', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_tenant_async.metadata = {'url': '/tenants'} diff --git a/azext_iot/sdk/pnp/modelrepository/models/__init__.py b/azext_iot/sdk/pnp/modelrepository/models/__init__.py deleted file mode 100644 index c835d715f..000000000 --- a/azext_iot/sdk/pnp/modelrepository/models/__init__.py +++ /dev/null @@ -1,28 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -try: - from .subject_metadata_py3 import SubjectMetadata - from .subject_py3 import Subject - from .target_py3 import Target - from .tenant_py3 import Tenant -except (SyntaxError, ImportError): - from .subject_metadata import SubjectMetadata - from .subject import Subject - from .target import Target - from .tenant import Tenant - -__all__ = [ - 'SubjectMetadata', - 'Subject', - 'Target', - 'Tenant', -] diff --git a/azext_iot/sdk/pnp/modelrepository/models/subject.py b/azext_iot/sdk/pnp/modelrepository/models/subject.py deleted file mode 100644 index e0c712547..000000000 --- a/azext_iot/sdk/pnp/modelrepository/models/subject.py +++ /dev/null @@ -1,40 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Subject(Model): - """Target. - - :param subject_type: Gets or sets the type. Possible values include: - 'User', 'ServicePrincipal' - :type subject_type: str or ~pnp.models.enum - :param role: Gets or sets Roles. Possible values include: - 'ModelsPublisher', 'ModelsCreator', 'TenantAdministrator', - 'ModelAdministrator', 'ModelReader', 'None' - :type role: str or ~pnp.models.enum - :param resource_type: Gets or sets the type of the resource. Possible - values include: 'Model', 'Tenant' - :type resource_type: str or ~pnp.models.enum - """ - - _attribute_map = { - 'subject_type': {'key': 'subjectType', 'type': 'str'}, - 'role': {'key': 'role', 'type': 'str'}, - 'resource_type': {'key': 'resourceType', 'type': 'str'}, - } - - def __init__(self, **kwargs): - super(Subject, self).__init__(**kwargs) - self.subject_type = kwargs.get('subject_type', None) - self.role = kwargs.get('role', None) - self.resource_type = kwargs.get('resource_type', None) diff --git a/azext_iot/sdk/pnp/modelrepository/models/subject_metadata.py b/azext_iot/sdk/pnp/modelrepository/models/subject_metadata.py deleted file mode 100644 index 45237cdc9..000000000 --- a/azext_iot/sdk/pnp/modelrepository/models/subject_metadata.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class SubjectMetadata(Model): - """Subject Metadata. - - :param resource_id: Gets or sets the resource identifier. - :type resource_id: str - :param subject_id: Gets or sets the subject identifier. - :type subject_id: str - """ - - _attribute_map = { - 'resource_id': {'key': 'resourceId', 'type': 'str'}, - 'subject_id': {'key': 'subjectId', 'type': 'str'}, - } - - def __init__(self, **kwargs): - super(SubjectMetadata, self).__init__(**kwargs) - self.resource_id = kwargs.get('resource_id', None) - self.subject_id = kwargs.get('subject_id', None) diff --git a/azext_iot/sdk/pnp/modelrepository/models/subject_metadata_py3.py b/azext_iot/sdk/pnp/modelrepository/models/subject_metadata_py3.py deleted file mode 100644 index 94dd4896b..000000000 --- a/azext_iot/sdk/pnp/modelrepository/models/subject_metadata_py3.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class SubjectMetadata(Model): - """Subject Metadata. - - :param resource_id: Gets or sets the resource identifier. - :type resource_id: str - :param subject_id: Gets or sets the subject identifier. - :type subject_id: str - """ - - _attribute_map = { - 'resource_id': {'key': 'resourceId', 'type': 'str'}, - 'subject_id': {'key': 'subjectId', 'type': 'str'}, - } - - def __init__(self, *, resource_id: str=None, subject_id: str=None, **kwargs) -> None: - super(SubjectMetadata, self).__init__(**kwargs) - self.resource_id = resource_id - self.subject_id = subject_id diff --git a/azext_iot/sdk/pnp/modelrepository/models/subject_py3.py b/azext_iot/sdk/pnp/modelrepository/models/subject_py3.py deleted file mode 100644 index 38e945858..000000000 --- a/azext_iot/sdk/pnp/modelrepository/models/subject_py3.py +++ /dev/null @@ -1,40 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Subject(Model): - """Target. - - :param subject_type: Gets or sets the type. Possible values include: - 'User', 'ServicePrincipal' - :type subject_type: str or ~pnp.models.enum - :param role: Gets or sets Roles. Possible values include: - 'ModelsPublisher', 'ModelsCreator', 'TenantAdministrator', - 'ModelAdministrator', 'ModelReader', 'None' - :type role: str or ~pnp.models.enum - :param resource_type: Gets or sets the type of the resource. Possible - values include: 'Model', 'Tenant' - :type resource_type: str or ~pnp.models.enum - """ - - _attribute_map = { - 'subject_type': {'key': 'subjectType', 'type': 'str'}, - 'role': {'key': 'role', 'type': 'str'}, - 'resource_type': {'key': 'resourceType', 'type': 'str'}, - } - - def __init__(self, *, subject_type=None, role=None, resource_type=None, **kwargs) -> None: - super(Subject, self).__init__(**kwargs) - self.subject_type = subject_type - self.role = role - self.resource_type = resource_type diff --git a/azext_iot/sdk/pnp/modelrepository/models/target.py b/azext_iot/sdk/pnp/modelrepository/models/target.py deleted file mode 100644 index a48f54887..000000000 --- a/azext_iot/sdk/pnp/modelrepository/models/target.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Target(Model): - """Target class. - - :param subject_metadata: Gets or sets the subject meta data. - :type subject_metadata: ~pnp.models.SubjectMetadata - :param subject: Gets or sets the subject. - :type subject: ~pnp.models.Subject - """ - - _attribute_map = { - 'subject_metadata': {'key': 'subjectMetadata', 'type': 'SubjectMetadata'}, - 'subject': {'key': 'subject', 'type': 'Subject'}, - } - - def __init__(self, **kwargs): - super(Target, self).__init__(**kwargs) - self.subject_metadata = kwargs.get('subject_metadata', None) - self.subject = kwargs.get('subject', None) diff --git a/azext_iot/sdk/pnp/modelrepository/models/target_py3.py b/azext_iot/sdk/pnp/modelrepository/models/target_py3.py deleted file mode 100644 index ea6ea322e..000000000 --- a/azext_iot/sdk/pnp/modelrepository/models/target_py3.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Target(Model): - """Target class. - - :param subject_metadata: Gets or sets the subject meta data. - :type subject_metadata: ~pnp.models.SubjectMetadata - :param subject: Gets or sets the subject. - :type subject: ~pnp.models.Subject - """ - - _attribute_map = { - 'subject_metadata': {'key': 'subjectMetadata', 'type': 'SubjectMetadata'}, - 'subject': {'key': 'subject', 'type': 'Subject'}, - } - - def __init__(self, *, subject_metadata=None, subject=None, **kwargs) -> None: - super(Target, self).__init__(**kwargs) - self.subject_metadata = subject_metadata - self.subject = subject diff --git a/azext_iot/sdk/pnp/modelrepository/models/tenant.py b/azext_iot/sdk/pnp/modelrepository/models/tenant.py deleted file mode 100644 index 521a9e363..000000000 --- a/azext_iot/sdk/pnp/modelrepository/models/tenant.py +++ /dev/null @@ -1,51 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Tenant(Model): - """Tenant Class. - - :param azure_ad_tenant_name: Gets or sets the name of the azure ad tenant. - :type azure_ad_tenant_name: str - :param azure_ad_tenant_id: Gets or sets the azure ad tenant identifier. - :type azure_ad_tenant_id: str - :param microsoft_partner_network_id: Gets or sets the microsoft partner - network identifier. - :type microsoft_partner_network_id: str - :param number_of_models_created: Gets or sets the number of models - created. - :type number_of_models_created: int - :param number_of_models_published: Gets or sets the number of models - published. - :type number_of_models_published: int - :param tenant_id: Gets or sets the tenant identifier. - :type tenant_id: str - """ - - _attribute_map = { - 'azure_ad_tenant_name': {'key': 'azureAdTenantName', 'type': 'str'}, - 'azure_ad_tenant_id': {'key': 'azureAdTenantId', 'type': 'str'}, - 'microsoft_partner_network_id': {'key': 'microsoftPartnerNetworkId', 'type': 'str'}, - 'number_of_models_created': {'key': 'numberOfModelsCreated', 'type': 'int'}, - 'number_of_models_published': {'key': 'numberOfModelsPublished', 'type': 'int'}, - 'tenant_id': {'key': 'tenantId', 'type': 'str'}, - } - - def __init__(self, **kwargs): - super(Tenant, self).__init__(**kwargs) - self.azure_ad_tenant_name = kwargs.get('azure_ad_tenant_name', None) - self.azure_ad_tenant_id = kwargs.get('azure_ad_tenant_id', None) - self.microsoft_partner_network_id = kwargs.get('microsoft_partner_network_id', None) - self.number_of_models_created = kwargs.get('number_of_models_created', None) - self.number_of_models_published = kwargs.get('number_of_models_published', None) - self.tenant_id = kwargs.get('tenant_id', None) diff --git a/azext_iot/sdk/pnp/modelrepository/models/tenant_py3.py b/azext_iot/sdk/pnp/modelrepository/models/tenant_py3.py deleted file mode 100644 index 43ab56fd9..000000000 --- a/azext_iot/sdk/pnp/modelrepository/models/tenant_py3.py +++ /dev/null @@ -1,51 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Tenant(Model): - """Tenant Class. - - :param azure_ad_tenant_name: Gets or sets the name of the azure ad tenant. - :type azure_ad_tenant_name: str - :param azure_ad_tenant_id: Gets or sets the azure ad tenant identifier. - :type azure_ad_tenant_id: str - :param microsoft_partner_network_id: Gets or sets the microsoft partner - network identifier. - :type microsoft_partner_network_id: str - :param number_of_models_created: Gets or sets the number of models - created. - :type number_of_models_created: int - :param number_of_models_published: Gets or sets the number of models - published. - :type number_of_models_published: int - :param tenant_id: Gets or sets the tenant identifier. - :type tenant_id: str - """ - - _attribute_map = { - 'azure_ad_tenant_name': {'key': 'azureAdTenantName', 'type': 'str'}, - 'azure_ad_tenant_id': {'key': 'azureAdTenantId', 'type': 'str'}, - 'microsoft_partner_network_id': {'key': 'microsoftPartnerNetworkId', 'type': 'str'}, - 'number_of_models_created': {'key': 'numberOfModelsCreated', 'type': 'int'}, - 'number_of_models_published': {'key': 'numberOfModelsPublished', 'type': 'int'}, - 'tenant_id': {'key': 'tenantId', 'type': 'str'}, - } - - def __init__(self, *, azure_ad_tenant_name: str=None, azure_ad_tenant_id: str=None, microsoft_partner_network_id: str=None, number_of_models_created: int=None, number_of_models_published: int=None, tenant_id: str=None, **kwargs) -> None: - super(Tenant, self).__init__(**kwargs) - self.azure_ad_tenant_name = azure_ad_tenant_name - self.azure_ad_tenant_id = azure_ad_tenant_id - self.microsoft_partner_network_id = microsoft_partner_network_id - self.number_of_models_created = number_of_models_created - self.number_of_models_published = number_of_models_published - self.tenant_id = tenant_id diff --git a/azext_iot/sdk/pnp/modelrepository/version.py b/azext_iot/sdk/pnp/modelrepository/version.py deleted file mode 100644 index bc2258ad0..000000000 --- a/azext_iot/sdk/pnp/modelrepository/version.py +++ /dev/null @@ -1,13 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -VERSION = "2020-05-01" - diff --git a/azext_iot/tests/pnp/__init__.py b/azext_iot/tests/pnp/__init__.py deleted file mode 100644 index f8e676359..000000000 --- a/azext_iot/tests/pnp/__init__.py +++ /dev/null @@ -1,15 +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 azure.cli.testsdk import LiveScenarioTest - - -class PNPLiveScenarioTest(LiveScenarioTest): - def __init__(self, test_scenario): - assert test_scenario - - super(PNPLiveScenarioTest, self).__init__(test_scenario) - PNPLiveScenarioTest.handle = self diff --git a/azext_iot/tests/pnp/test_iot_pnp_int.py b/azext_iot/tests/pnp/test_iot_pnp_int.py deleted file mode 100644 index 1bfbfaa49..000000000 --- a/azext_iot/tests/pnp/test_iot_pnp_int.py +++ /dev/null @@ -1,303 +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. -# -------------------------------------------------------------------------------------------- - -import pytest -import json -import os - -from io import open -from os.path import exists -from . import PNPLiveScenarioTest -from azext_iot.common.utility import read_file_content -from azext_iot.common.embedded_cli import EmbeddedCLI -from azext_iot.pnp.common import RoleIdentifier -from knack.util import CLIError - -_capability_model_payload = "test_pnp_create_payload_model.json" -_pnp_dns_suffix = "azureiotrepository-test.com" - - -@pytest.mark.usefixtures("set_cwd") -class TestPnPModelLifecycle(PNPLiveScenarioTest): - def __init__(self, _): - super(TestPnPModelLifecycle, self).__init__(_) - account_settings = EmbeddedCLI().invoke("account show").as_json()["user"] - - repo_id = ( - EmbeddedCLI() - .invoke("iot pnp repo list --pnp-dns-suffix {}".format(_pnp_dns_suffix)) - .as_json()[0]["tenantId"] - ) - - self.kwargs.update( - { - "model": "test_model_definition.json", - "user_id": account_settings["name"], - "user_type": account_settings["type"], - "repo_id": repo_id, - "pnp_dns_suffix": _pnp_dns_suffix, - } - ) - - def setUp(self): - if self._testMethodName == "test_model_lifecycle": - - roles = self.cmd( - "iot pnp role-assignment list --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " - "--pnp-dns-suffix {pnp_dns_suffix}" - ) - # check for TenantAdministrator - try: - roles = roles.get_output_in_json() - role_assignments = list( - map(lambda role: role["subject"]["role"], roles) - ) - if RoleIdentifier.tenantAdmin.value not in role_assignments: - self.skipTest("Need TenantAdmin role to perform tests") - except CLIError as e: - self.skipTest(e) - - # Assign roles for model test - - self.cmd( - "iot pnp role-assignment create --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " - "--subject-type {user_type} --role ModelsCreator --pnp-dns-suffix {pnp_dns_suffix}" - ) - - self.cmd( - "iot pnp role-assignment create --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " - "--subject-type {user_type} --role ModelsPublisher --pnp-dns-suffix {pnp_dns_suffix}" - ) - - # Generate model ID - - model = str(read_file_content(_capability_model_payload)) - _model_id = self._generate_model_id(json.loads(model)["@id"]) - self.kwargs.update({"model_id": _model_id}) - model_newContent = model.replace( - json.loads(model)["@id"], self.kwargs["model_id"] - ) - model_newContent = model_newContent.replace("\n", "") - - fo = open(self.kwargs["model"], "w+", encoding="utf-8") - fo.write(model_newContent) - fo.close() - - def tearDown(self): - if exists(self.kwargs["model"]): - os.remove(self.kwargs["model"]) - - if self._testMethodName == "test_model_lifecycle": - - # RBAC for model integration tests (create, show, publish models in tenant) - - self.cmd( - "iot pnp role-assignment delete --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " - "--role ModelsCreator --pnp-dns-suffix {pnp_dns_suffix}" - ) - - self.cmd( - "iot pnp role-assignment delete --resource-id {repo_id} --resource-type Tenant --subject-id {user_id} " - "--role ModelsPublisher --pnp-dns-suffix {pnp_dns_suffix}" - ) - - def _generate_model_id(self, model_id): - from datetime import datetime - - now = datetime.now() - date_str = now.strftime("test%m%d%H") - time_str = now.strftime("%M").strip("0") - return "{}:{};{}".format(model_id, date_str, time_str) - - def test_model_lifecycle(self): - - # Error: Invalid model definition file - self.cmd( - "iot pnp model create --model '' --pnp-dns-suffix {pnp_dns_suffix}", - expect_failure=True, - ) - - # Error: wrong path of model definition - self.cmd( - "iot pnp model create --model model.json --pnp-dns-suffix {pnp_dns_suffix}", - expect_failure=True, - ) - - # Success: Create new model - created = self.cmd( - "iot pnp model create --model {model} --pnp-dns-suffix {pnp_dns_suffix}" - ).get_output_in_json() - - assert created["@id"] == self.kwargs["model_id"] - - # Checking the model list - self.cmd( - "iot pnp model list --pnp-dns-suffix {pnp_dns_suffix}", - checks=[ - self.greater_than("length([*])", 0), - self.exists("[?modelId==`{}`]".format(self.kwargs["model_id"])), - ], - ) - - # Get model - model = self.cmd( - "iot pnp model show --model-id {model_id} --pnp-dns-suffix {pnp_dns_suffix}" - ).get_output_in_json() - assert json.dumps(model) - assert model["@id"] == self.kwargs["model_id"] - - # Publish model - published = self.cmd( - "iot pnp model publish --model-id {model_id} --pnp-dns-suffix {pnp_dns_suffix} --yes" - ).get_output_in_json() - assert json.dumps(published) - assert published["@id"] == self.kwargs["model_id"] - - # Checking the model list for published model - self.cmd( - "iot pnp model list -q {model_id} --state Listed --pnp-dns-suffix {pnp_dns_suffix}", - checks=[ - self.greater_than("length([*])", 0), - self.exists("[?modelId==`{}`]".format(self.kwargs["model_id"])), - ], - ) - - -class TestPNPRepo(PNPLiveScenarioTest): - def __init__(self, test_case): - account = EmbeddedCLI().invoke("account show").as_json() - self.user_id = account["user"]["name"] - self.user_type = account["user"]["type"] - super(TestPNPRepo, self).__init__(test_case) - - def setUp(self): - if self._testMethodName == "test_repo_rbac": - # check for TenantAdministrator - try: - repo_id = ( - EmbeddedCLI() - .invoke( - "iot pnp repo list --pnp-dns-suffix {}".format(_pnp_dns_suffix) - ) - .as_json()[0]["tenantId"] - ) - roles = self.cmd( - "iot pnp role-assignment list --resource-id {0} --resource-type Tenant --subject-id {1} " - "--pnp-dns-suffix {2}".format( - repo_id, self.user_id, _pnp_dns_suffix - ) - ) - roles = roles.get_output_in_json() - role_assignments = list( - map(lambda role: role["subject"]["role"], roles) - ) - if RoleIdentifier.tenantAdmin.value not in role_assignments: - self.skipTest("Need TenantAdmin role to perform test") - except CLIError as e: - self.skipTest(e) - - @pytest.mark.skipif(True, reason="Create not functional at the moment") - def test_repo_create(self): - - # create repo - - repo = self.cmd( - "iot pnp repo create --pnp-dns-suffix {pnp_dns_suffix}" - ).get_output_in_json() - - repo_id = repo["tenantId"] - - # list repos - - repos = self.cmd( - "az iot pnp repo list --pnp-dns-suffix {pnp_dns_suffix}" - ).get_output_in_json() - - assert len(repos) == 1 - assert repos[0]["tenantId"] == repo_id - - # get role assignments for repo, should only be one (tenant admin) - - role_assignments = self.cmd( - "az iot pnp role-assignment list --resource-id {0} --resource-type Tenant --pnp-dns-suffix {1}".format( - repo_id, _pnp_dns_suffix - ) - ).get_output_in_json() - - assert len(role_assignments) == 1 - assert role_assignments[0]["subjectMetadata"]["subjectId"] == self.user_id - assert ( - role_assignments[0]["subject"]["role"] == RoleIdentifier.tenantAdmin.value - ) - - def test_repo_rbac(self): - - # get repo - - repos = self.cmd( - "az iot pnp repo list --pnp-dns-suffix {}".format(_pnp_dns_suffix) - ).get_output_in_json() - - repo_id = repos[0]["tenantId"] - - # add role assignment for repo (tenant) - new_role = RoleIdentifier.modelsCreator.value - self.cmd( - "az iot pnp role-assignment create --resource-id {0} --resource-type Tenant " - "--subject-id {1} --subject-type {2} --role {3} --pnp-dns-suffix {4}".format( - repo_id, self.user_id, self.user_type, new_role, _pnp_dns_suffix - ) - ) - - # get newest role assignments for user - - role_assignments = self.cmd( - "az iot pnp role-assignment list --resource-id {0} --resource-type Tenant --subject-id {1} " - "--pnp-dns-suffix {2}".format(repo_id, self.user_id, _pnp_dns_suffix) - ).get_output_in_json() - - # ensure our new role exists - - assert ( - len( - [ - role - for role in role_assignments - if role["subjectMetadata"]["subjectId"] == self.user_id - and role["subject"]["role"] == new_role - ] - ) - == 1 - ) - - # delete role assignment - - self.cmd( - "az iot pnp role-assignment delete --resource-id {0} --resource-type Tenant --role {1} --subject {2} " - "--pnp-dns-suffix {3}".format( - repo_id, new_role, self.user_id, _pnp_dns_suffix - ) - ) - - # get assignments again - - role_assignments = self.cmd( - "az iot pnp role-assignment list --resource-id {0} --resource-type Tenant --subject-id {1} " - "--pnp-dns-suffix {2}".format(repo_id, self.user_id, _pnp_dns_suffix) - ).get_output_in_json() - - # ensure our new role does not exist - assert ( - len( - [ - role - for role in role_assignments - if role["subjectMetadata"]["subjectId"] == self.user_id - and role["subject"]["role"] == new_role - ] - ) - == 0 - ) diff --git a/azext_iot/tests/pnp/test_iot_pnp_unit.py b/azext_iot/tests/pnp/test_iot_pnp_unit.py deleted file mode 100644 index 67c034ae1..000000000 --- a/azext_iot/tests/pnp/test_iot_pnp_unit.py +++ /dev/null @@ -1,513 +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. -# -------------------------------------------------------------------------------------------- - -import pytest -import json - -from azext_iot.pnp import commands_api as dataplane, commands_repository as repo -from azext_iot.pnp.common import RoleIdentifier -from azext_iot.common.utility import url_encode_str, read_file_content -from knack.util import CLIError -from ..conftest import fixture_cmd, path_service_client, build_mock_response - - -_pnp_create_model_payload_file = "test_pnp_create_payload_model.json" -_pnp_generic_model_id = "urn:example:capabilityModels:Mxchip:1" - -mock_target = {} - - -def generate_pnp_model_create_payload(content_from_file=False): - if content_from_file: - return (None, _pnp_create_model_payload_file) - - return ( - str(read_file_content(_pnp_create_model_payload_file)), - _pnp_create_model_payload_file, - ) - - -@pytest.mark.usefixtures("set_cwd") -class TestModelRepoModelCreate(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, request, set_cwd): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.mark.parametrize( - "content_from_file", [True, False], - ) - def test_model_create(self, fixture_cmd, serviceclient, content_from_file, set_cwd): - - payload = None - payload_scenario = generate_pnp_model_create_payload(content_from_file) - # If file path provided - if not payload_scenario[0]: - payload = payload_scenario[1] - payload = str(read_file_content(_pnp_create_model_payload_file)) - - model_id = json.loads(payload)["@id"] - dataplane.iot_pnp_model_create( - cmd=fixture_cmd, model=payload, - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - data = args[0][0].data - - assert method == "PUT" - assert "/models/{}?".format(url_encode_str(model_id, plus=True)) in url - assert json.dumps(data) - - @pytest.mark.parametrize( - "content_from_file", [True, False], - ) - def test_model_create_error( - self, fixture_cmd, serviceclient_generic_error, content_from_file - ): - payload = None - payload_scenario = generate_pnp_model_create_payload(content_from_file) - if not payload_scenario[0]: - payload = payload_scenario[1] - payload = str(read_file_content(_pnp_create_model_payload_file)) - - payload = json.loads(payload) - del payload["@id"] - payload = json.dumps(payload) - with pytest.raises(CLIError): - dataplane.iot_pnp_model_create( - fixture_cmd, model=payload, - ) - - @pytest.mark.parametrize("model_id, exp", [("", CLIError)]) - def test_model_create_invalid_args(self, serviceclient, model_id, exp): - with pytest.raises(exp): - dataplane.iot_pnp_model_create(fixture_cmd, model="{{}") - - def test_model_create_invalid_payload(self, serviceclient): - - payload = str(read_file_content(_pnp_create_model_payload_file)) - payload = json.loads(payload) - del payload["@id"] - payload = json.dumps(payload) - with pytest.raises(CLIError): - dataplane.iot_pnp_model_create(fixture_cmd, model=payload) - - -class TestModelRepoModelPublish(object): - @pytest.fixture(params=[400, 418, 503]) - def serviceclient_publish_errors(self, mocker, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response( - mocker, request.param, {"error": "something failed"} - ) - return service_client - - @pytest.fixture(params=[(200, 200, 201), (200, 200, 204), (200, 200, 412)]) - def serviceclient(self, mocker, request): - service_client = mocker.patch(path_service_client) - payload_list = { - "continuationToken": "null", - "results": [ - { - "comment": "", - "createdOn": "2019-07-09T07:46:06.044161+00:00", - "description": "", - "displayName": "Mxchip 1", - "etag": '"41006e67-0000-0800-0000-5d2501b80000"', - "modelName": "example:capabilityModels:Mxchip", - "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb", - "publisherName": "microsoft.com", - "type": "CapabilityModel", - "updatedOn": "2019-07-09T21:06:00.072063+00:00", - "urnId": "urn:example:capabilityModels:Mxchip:1", - "version": 1, - } - ], - } - payload_show = { - "@id": "urn:example:capabilityModels:Mxchip:1", - "@type": "CapabilityModel", - "displayName": "Mxchip 1", - "implements": [ - {"schema": "urn:example:interfaces:MXChip:1", "name": "MXChip1"} - ], - "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json", - } - test_side_effect = [ - build_mock_response( - mocker, request.param[0], payload=payload_list, headers={"eTag": "eTag"} - ), - build_mock_response( - mocker, request.param[1], payload=payload_show, headers={"eTag": "eTag"} - ), - build_mock_response(mocker, request.param[2], {}, headers={"eTag": "eTag"}), - ] - service_client.side_effect = test_side_effect - return service_client - - @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) - def test_model_publish(self, fixture_cmd, serviceclient, target_model): - dataplane.iot_pnp_model_publish( - fixture_cmd, model_id=target_model, - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - headers = args[0][0].headers - - assert method == "PUT" - assert "/models/{}?".format(url_encode_str(target_model, plus=True)) in url - assert "update-metadata=true" in url - assert headers.get("x-ms-model-state") == "Listed" - - @pytest.mark.parametrize("target_model", [("acv.17")]) - def test_model_publish_error( - self, fixture_cmd, serviceclient_publish_errors, target_model - ): - with pytest.raises(CLIError): - dataplane.iot_pnp_model_publish( - fixture_cmd, model_id=target_model, - ) - - -class TestModelRepoModelShow(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, request): - service_client = mocker.patch(path_service_client) - payload = { - "@id": "urn:example:capabilityModels:Mxchip:1", - "@type": "CapabilityModel", - "displayName": "Mxchip 1", - "implements": [ - {"schema": "urn:example:interfaces:MXChip:1", "name": "MXChip1"} - ], - "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json", - } - service_client.return_value = build_mock_response( - mocker, request.param, payload=payload - ) - return service_client - - @pytest.fixture(params=[404, 500]) - def serviceclient_generic_error(self, mocker, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response( - mocker, request.param, {"error": "something failed"} - ) - return service_client - - @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) - def test_model_show(self, fixture_cmd, serviceclient, target_model): - result = dataplane.iot_pnp_model_show(fixture_cmd, model_id=target_model) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - - assert method == "GET" - assert "/models/{}?".format(url_encode_str(target_model, plus=True)) in url - assert json.dumps(result) - - @pytest.mark.parametrize("target_model", [("acv:17")]) - def test_model_show_error( - self, fixture_cmd, serviceclient_generic_error, target_model - ): - response = dataplane.iot_pnp_model_show(fixture_cmd, model_id=target_model) - assert "error" in response - - @pytest.fixture(params=[400]) - def serviceclientemptyresult(self, mocker, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)]) - def test_model_show_no_result( - self, fixture_cmd, serviceclientemptyresult, target_model - ): - with pytest.raises(CLIError): - dataplane.iot_pnp_model_show( - fixture_cmd, model_id=target_model, - ) - - -class TestModelRepoModelList(object): - @pytest.fixture(params=[200]) - def service_client(self, mocker, request): - serviceclient = mocker.patch(path_service_client) - payload = json.dumps( - [ - { - "_etag": '"00000000-0000-0800-0000-000000000000"', - "comment": "", - "countOfCommands": 0, - "countOfComponents": 0, - "countOfExtends": 0, - "countOfProperties": 0, - "countOfRelationships": 0, - "countOfSchemas": 0, - "countOfTelemetries": 0, - "createdBy": "user@contoso.com", - "createdDate": "2020-04-29T17:17:44.3055434+00:00", - "displayName": "Device Information", - "modelId": "test:model:name;1", - "modelName": "test:model:name", - "modelState": "Listed", - "modelType": "Interface", - "publisherId": "00000000-0000-0000-0000-000000000000", - "publisherName": "user@contoso.com", - "updatedDate": "2020-04-29T17:24:48.8645696+00:00", - "version": "1", - } - ] - ) - serviceclient.return_value = build_mock_response(mocker, request.param, payload) - return serviceclient - - @pytest.fixture(params=[400, 503]) - def serviceclient_generic_error(self, mocker, fixture_sas, request): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response( - mocker, request.param, {"error": "something failed"} - ) - return service_client - - @pytest.mark.parametrize( - ["search_content", "shared"], - [ - ( - { - "searchKeyword": "keyword", - "modelType": "type", - "modelState": "state", - "publisherId": "publisher", - }, - False, - ), - ({"searchKeyword": "test"}, True), - ({}, False), - ], - ) - def test_model_list(self, fixture_cmd, service_client, search_content, shared): - result = dataplane.iot_pnp_model_list( - fixture_cmd, - keyword=search_content.get("searchKeyword"), - model_type=search_content.get("modelType"), - model_state=search_content.get("modelState"), - publisher_id=search_content.get("publisherId"), - shared=shared, - ) - args = service_client.call_args - url = args[0][0].url - method = args[0][0].method - headers = args[0][0].headers - body = json.loads(args[0][0].body) - - for k in search_content.keys(): - assert body[k] == search_content[k] - - assert ( - headers.get("x-ms-show-shared-models-only") == "true" if shared else "false" - ) - assert method == "POST" - - assert "/models/search?" in url - assert len(result) == 1 - - def test_model_list_error(self, fixture_cmd, serviceclient_generic_error): - with pytest.raises(CLIError): - dataplane.iot_pnp_model_list(fixture_cmd,) - - -class TestModelRepoRepoCreate(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, request, set_cwd): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - def test_repo_create(self, fixture_cmd, serviceclient): - repo.iot_pnp_tenant_create(fixture_cmd) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - - assert "/tenants" in url - assert method == "PUT" - - -class TestModelRepoRepoList(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, request, set_cwd): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, [{}]) - return service_client - - def test_repo_list(self, fixture_cmd, serviceclient): - repo.iot_pnp_tenant_show(fixture_cmd) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - - assert "/tenants" in url - assert method == "GET" - - -class TestModelRepoRBAC(object): - @pytest.fixture(params=[200]) - def serviceclient(self, mocker, request, set_cwd): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, {}) - return service_client - - @pytest.fixture(params=[200]) - def serviceclient_arr(self, mocker, request, set_cwd): - service_client = mocker.patch(path_service_client) - service_client.return_value = build_mock_response(mocker, request.param, [{}]) - return service_client - - @pytest.mark.parametrize( - ["role", "resource_id", "resource_type", "subject_id", "subject_type"], - [ - ( - RoleIdentifier.modelAdmin, - "12345-12345-12345-12345", - "Tenant", - "user@tenant.com", - "user", - ), - ( - RoleIdentifier.modelReader, - "abc:123:model;2", - "Model", - "user@tenant.com", - "user", - ), - ( - RoleIdentifier.modelsCreator, - "12345-12345-12345-12345", - "Tenant", - "12345-12345-12345", - "ServicePrincipal", - ), - ( - RoleIdentifier.modelsPublisher, - "abc:123:model;2", - "Model", - "12345-12345-12345", - "ServicePrincipal", - ), - ], - ) - def test_repo_role_create( - self, - fixture_cmd, - serviceclient, - role, - resource_id, - resource_type, - subject_id, - subject_type, - ): - - resource_id = "resource_id" - resource_type = "resource_type" - subject_id = "subject" - subject_type = "subject_type" - - repo.iot_pnp_role_create( - fixture_cmd, resource_id, resource_type, subject_id, subject_type, role - ) - args = serviceclient.call_args - url = args[0][0].url - method = args[0][0].method - data = json.loads(args[0][0].data) - - assert method == "PUT" - assert ( - "/resources/{0}/types/{1}/subjects/{2}".format( - resource_id, resource_type, subject_id - ) - in url - ) - assert data["subjectType"] == subject_type - assert data["role"] == role.value - assert data["resourceType"] == resource_type - - @pytest.mark.parametrize( - ["resource_id", "resource_type", "subject_id"], - [ - ("12345-12345-12345", "Tenant", "user@tenant.com"), - ("test:model:perms;1", "Model", "12345-12345-12345"), - ("12345-12345-12345", "Tenant", None), - ("test:model:perms;1", "Model", None), - ], - ) - def test_repo_role_show( - self, fixture_cmd, serviceclient_arr, resource_id, resource_type, subject_id - ): - - repo.iot_pnp_role_list(fixture_cmd, resource_id, resource_type, subject_id) - args = serviceclient_arr.call_args - url = args[0][0].url - method = args[0][0].method - - assert method == "GET" - assert ( - ( - "/resources/{0}/types/{1}/subjects/{2}".format( - url_encode_str(resource_id), - resource_type, - url_encode_str(subject_id), - ) - in url - ) - if subject_id - else ( - "/resources/{0}/types/{1}".format( - url_encode_str(resource_id), resource_type - ) - in url - ) - ) - - @pytest.mark.parametrize( - ["role", "resource_id", "resource_type", "subject_id"], - [ - ( - RoleIdentifier.modelAdmin, - "12345-12345-12345-12345", - "Tenant", - "user@tenant.com", - ), - ( - RoleIdentifier.modelReader, - "abc:123:model;2", - "Model", - "12345-12345-12345", - ), - ], - ) - def test_repo_role_delete( - self, fixture_cmd, serviceclient, role, resource_id, resource_type, subject_id, - ): - - repo.iot_pnp_role_delete( - fixture_cmd, resource_id, resource_type, role, subject_id - ) - args = serviceclient.call_args - url = args[0][0].url - - assert ( - "/resources/{0}/types/{1}/subjects/{2}/roles/{3}".format( - url_encode_str(resource_id), - resource_type, - url_encode_str(subject_id), - role.value, - ) - in url - ) diff --git a/azext_iot/tests/pnp/test_pnp_create_payload_interface.json b/azext_iot/tests/pnp/test_pnp_create_payload_interface.json deleted file mode 100644 index feb634998..000000000 --- a/azext_iot/tests/pnp/test_pnp_create_payload_interface.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "@id": "urn:example:interfaces:MXChip:1", - "@type": "Interface", - "displayName": "MXChip1", - "contents": [{ - "@type": "Property", - "name": "dieNumber", - "displayName": "Die Number", - "schema": "double" - }, - { - "@type": "Property", - "name": "setCurrent", - "displayName": "Current", - "writable": true, - "schema": "double", - "displayUnit": "amps" - }, - { - "@type": "Property", - "name": "setVoltage", - "displayName": "Voltage", - "writable": true, - "schema": "double", - "displayUnit": "volts" - }, - { - "@type": "Property", - "name": "fanSpeed", - "displayName": "Fan Speed", - "writable": true, - "schema": "double", - "displayUnit": "rpm" - }, - { - "@type": "Property", - "name": "activateIR", - "displayName": "IR", - "writable": true, - "schema": "boolean" - }, - { - "@type": "Telemetry", - "name": "humidity", - "displayName": "Humidity", - "schema": "double", - "displayUnit": "%" - }, - { - "@type": "Telemetry", - "name": "pressure", - "displayName": "Pressure", - "schema": "double", - "displayUnit": "hPa" - }, - { - "@type": "Telemetry", - "name": "magnetometer", - "displayName": "Magnetometer", - "schema": { - "@type": "Object", - "fields": [{ - "name": "x", - "schema": "double", - "displayUnit": "mgauss" - }, - { - "name": "y", - "schema": "double", - "displayUnit": "mgauss" - }, - { - "name": "z", - "schema": "double", - "displayUnit": "mgauss" - } - ] - } - }, - { - "@type": "Telemetry", - "name": "accelerometer", - "displayName": "Accelerometer", - "schema": { - "@type": "Object", - "fields": [{ - "name": "x", - "schema": "double", - "displayUnit": "mg" - }, - { - "name": "y", - "schema": "double", - "displayUnit": "mg" - }, - { - "name": "z", - "schema": "double", - "displayUnit": "mg" - } - ] - } - }, - { - "@type": "Telemetry", - "name": "gyroscope", - "displayName": "Gyroscope", - "schema": { - "@type": "Object", - "fields": [{ - "name": "x", - "schema": "double", - "displayUnit": "mdps" - }, - { - "name": "y", - "schema": "double", - "displayUnit": "mdps" - }, - { - "name": "z", - "schema": "double", - "displayUnit": "mdps" - } - ] - } - }, - { - "@type": "Telemetry", - "name": "buttonBPressed", - "displayName": "Button B Pressed", - "schema": "string" - }, - { - "@type": "Telemetry", - "name": "deviceState", - "displayName": "Device State", - "schema": { - "@type": "Enum", - "valueSchema": "string", - "enumValues": [{ - "name": "normal", - "displayName": "Normal", - "enumValue": "NORMAL" - }, - { - "name": "danger", - "displayName": "Danger", - "enumValue": "DANGER" - }, - { - "name": "caution", - "displayName": "Caution", - "enumValue": "CAUTION" - } - ] - } - }, - { - "@type": "Command", - "name": "echo", - "displayName": "Echo", - "request": { - "name": "name1", - "schema": { - "@type": "Object", - "fields": [{ - "name": "displayedValue", - "displayName": "Value to display", - "schema": "string" - }] - } - }, - "response": { - "name": "name2", - "schema": "string" - } - }, - { - "@type": "Command", - "name": "countdown", - "displayName": "Countdown", - "request": { - "name": "name1", - "schema": { - "@type": "Object", - "fields": [{ - "name": "countFrom", - "displayName": "Count from", - "schema": "double" - }] - } - }, - "response": { - "name": "name3", - "schema": "double" - } - } - ], - "@context": "http://azureiot.com/v1/contexts/IoTModel.json" -} \ No newline at end of file diff --git a/azext_iot/tests/pnp/test_pnp_create_payload_model.json b/azext_iot/tests/pnp/test_pnp_create_payload_model.json deleted file mode 100644 index a5bb86ca5..000000000 --- a/azext_iot/tests/pnp/test_pnp_create_payload_model.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "@id": "dtmi:iotpnpcli", - "@type": "Interface", - "displayName": "My Sample Root Interface", - "contents": [ - { - "@type": "Component", - "schema": "dtmi:Contoso1:EnvironmentalSensor;1", - "name": "sensor" - } - ], - "@context": "dtmi:dtdl:context;2" -} \ No newline at end of file diff --git a/azext_iot/tests/pnp/test_pnp_interface_show.json b/azext_iot/tests/pnp/test_pnp_interface_show.json deleted file mode 100644 index 1e93f496d..000000000 --- a/azext_iot/tests/pnp/test_pnp_interface_show.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "@context": "http://azureiot.com/v1/contexts/IoTModel.json", - "@id": "urn:contoso:com:EnvironmentalSensor:1", - "@type": "Interface", - "comment": "Requires temperature and humidity sensors.", - "contents": [{ - "@type": "Property", - "description": "The state of the device. Two states online/offline are available.", - "displayName": "Device State", - "name": "state", - "schema": "boolean" - }, - { - "@type": "Property", - "description": "The name of the customer currently operating the device.", - "displayName": "Customer Name", - "name": "name", - "schema": "string", - "writable": true - }, - { - "@type": "Property", - "description": "The brightness level for the light on the device. Can be specified as 1 (high), 2 (medium), 3 (low)", - "displayName": "Brightness Level", - "name": "brightness", - "schema": "long", - "writable": true - }, - { - "@type": [ - "Telemetry", - "SemanticType/Temperature" - ], - "description": "Current temperature on the device", - "displayName": "Temperature", - "name": "temp", - "schema": "double", - "unit": "Units/Temperature/fahrenheit" - }, - { - "@type": [ - "Telemetry", - "SemanticType/Humidity" - ], - "description": "Current humidity on the device", - "displayName": "Humidity", - "name": "humid", - "schema": "double", - "unit": "Units/Humidity/percent" - }, - { - "@type": "Command", - "commandType": "synchronous", - "description": "This command will begin blinking the LED for given time interval.", - "name": "blink", - "request": { - "name": "blinkRequest", - "schema": "long" - }, - "response": { - "name": "blinkResponse", - "schema": "string" - } - }, - { - "@type": "Command", - "commandType": "synchronous", - "comment": "This Commands will turn-on the LED light on the device.", - "name": "turnon", - "response": { - "name": "turnonResponse", - "schema": "string" - } - }, - { - "@type": "Command", - "commandType": "synchronous", - "comment": "This Commands will turn-off the LED light on the device.", - "name": "turnoff", - "response": { - "name": "turnoffResponse", - "schema": "string" - } - } - ], - "description": "Provides functionality to report temperature, humidity. Provides telemetry, commands and read-write properties", - "displayName": "Environmental Sensor" -} \ No newline at end of file diff --git a/setup.py b/setup.py index 8b1a03495..aac6711e7 100644 --- a/setup.py +++ b/setup.py @@ -69,6 +69,7 @@ setup( name=PACKAGE_NAME, version=VERSION, + python_requires=">=3.5,<4", description=short_description, long_description="{} Intended for power users and/or automation of IoT solutions at scale.".format(short_description), license="MIT", From c0d7f78be55fe5c0424235c09994d0dee9efb417 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 8 Oct 2020 16:36:28 -0700 Subject: [PATCH 125/179] Update README.md --- README.md | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7afbb269b..e8286e405 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Please refer to the [Installation Troubleshooting Guide](docs/install-help.md) i ## Usage -After installing the Azure IoT extension your CLI environment is augmented with the addition of `central`, `device`, `dps`, `dt`, `edge`, `hub` and `pnp` commands. +After installing the Azure IoT extension your CLI environment is augmented with the addition of `hub`, `central`, `dps`, `dt`, `edge` and `device` commands. For usage and help content of any command or command group, pass in the `-h` parameter. Root command group details are shown for the following IoT services. @@ -95,7 +95,7 @@ For more specific examples, use: az find "az iot central"
- IoT Device Provisioning Service + IoT Device Provisioning ``` $ az iot dps -h @@ -136,7 +136,7 @@ Commands: ```
-
+
IoT Hub ``` @@ -183,26 +183,6 @@ Commands: ```
-
- IoT Plug & Play - -``` -$ az iot pnp -h -Group - az iot pnp : Manage Azure IoT Plug-and-Play repositories and models. - -Subgroups: - model [Preview] : Create, view, and publish device models in your company - repository. - repo [Preview] : Create and view Azure IoT Plug-and-Play tenant - repositories. - role-assignment [Preview] : Manage and configure PnP repository and model role - assignments. - twin [Preview] : Manipulate and interact with the digital twin of an IoT - Hub device. -``` -
- ## Scenario Automation Please refer to the [Scenario Automation](docs/scenario-automation.md) page for examples of how to use the IoT extension in scripts. From ddf4f0beb510551999fc97dc5af7f4c7ed2d202f Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Fri, 9 Oct 2020 12:15:30 -0700 Subject: [PATCH 126/179] Fix hub device IT. --- azext_iot/tests/test_iot_ext_int.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index d85a514c5..733cb7971 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -493,10 +493,10 @@ def test_hub_devices(self): self.cmd( '''iot hub device-identity update -d {} -n {} -g {} --primary-key="" --secondary-key=""'''.format( - edge_device_ids[1], LIVE_HUB, LIVE_RG + device_ids[4], LIVE_HUB, LIVE_RG ), checks=[ - self.check("deviceId", edge_device_ids[1]), + self.check("deviceId", device_ids[4]), self.check("status", "enabled"), self.exists("authentication.symmetricKey.primaryKey"), self.exists("authentication.symmetricKey.secondaryKey"), @@ -507,10 +507,10 @@ def test_hub_devices(self): self.cmd( '''iot hub device-identity update -d {} --login {} --set authentication.symmetricKey.primaryKey="" authentication.symmetricKey.secondaryKey=""'''.format( - edge_device_ids[1], self.connection_string + device_ids[4], self.connection_string ), checks=[ - self.check("deviceId", edge_device_ids[1]), + self.check("deviceId", device_ids[4]), self.check("status", "enabled"), self.exists("authentication.symmetricKey.primaryKey"), self.exists("authentication.symmetricKey.secondaryKey"), From 9b7ca36ae0d7fefa8730a37bf0e3269b3cb74df6 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 20 Oct 2020 17:30:30 -0700 Subject: [PATCH 127/179] Short-term command table linter fix. Need to overhaul pipeline. --- .azure-devops/merge.yml | 8 +------ .../templates/install-configure-azure-cli.yml | 21 ++++++++++++++++--- azext_iot/monitor/telemetry.py | 2 +- azext_iot/product/test/command_tests.py | 2 +- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 6c78d30b7..dd16a2e9e 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -117,16 +117,10 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.x' + versionSpec: '3.6' architecture: 'x64' - template: templates/install-configure-azure-cli.yml - - template: templates/download-install-local-azure-iot-cli-extension.yml - - bash: | - set -ev - source ./$(iot_ext_venv)/bin/activate - cp ./linter_exclusions.yml $AZURE_EXTENSION_DIR/$(iot_ext_package)/ - azdev linter --include-whl-extensions $(iot_ext_package) --min-severity medium - job: CredScan displayName: "Credential Scan" diff --git a/.azure-devops/templates/install-configure-azure-cli.yml b/.azure-devops/templates/install-configure-azure-cli.yml index 59bd54559..34a5f5d9a 100644 --- a/.azure-devops/templates/install-configure-azure-cli.yml +++ b/.azure-devops/templates/install-configure-azure-cli.yml @@ -1,14 +1,29 @@ steps: + - task: DownloadBuildArtifacts@0 + displayName : 'Download Extension wheel from Build Artifacts' + inputs: + buildType: 'current' + downloadType: 'single' + artifactName: 'azure-iot' + downloadPath: '$(System.ArtifactsDirectory)/extension' - task: Bash@3 inputs: targetType: 'inline' script: | set -ev pip install virtualenv - python -m virtualenv $(iot_ext_venv)/ - source ./$(iot_ext_venv)/bin/activate + python -m virtualenv venv/ + source ./venv/bin/activate git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli pip install azdev azdev --version - azdev setup -c ../azure-cli + azdev setup -c ../azure-cli -r ./ AZURE_EXTENSION_DIR=~/.azure/cliextensions + ARTIFACTS_DIR=$(System.ArtifactsDirectory)/extension + WHEELS=$(ls $ARTIFACTS_DIR/azure-iot/*.whl) + az --version + for i in $WHEELS; do + az extension add --source $i -y --debug + done + cp ./linter_exclusions.yml $AZURE_EXTENSION_DIR/azure-iot/ + azdev linter --include-whl-extensions azure-iot --min-severity medium diff --git a/azext_iot/monitor/telemetry.py b/azext_iot/monitor/telemetry.py index a4e756e1c..c83f494a8 100644 --- a/azext_iot/monitor/telemetry.py +++ b/azext_iot/monitor/telemetry.py @@ -73,7 +73,7 @@ def start_multiple_monitors( result = loop.run_until_complete(future) except KeyboardInterrupt: print("Stopping event monitor...", flush=True) - for t in asyncio.Task.all_tasks(): + for t in asyncio.Task.all_tasks(): # pylint: disable=no-member t.cancel() loop.run_forever() finally: diff --git a/azext_iot/product/test/command_tests.py b/azext_iot/product/test/command_tests.py index d371746eb..7d73a351c 100644 --- a/azext_iot/product/test/command_tests.py +++ b/azext_iot/product/test/command_tests.py @@ -278,7 +278,7 @@ def _read_certificate_from_file(certificate_path): with open(file=certificate_path, mode="rb") as f: data = f.read() - from base64 import encodestring + from base64 import encodestring # pylint: disable=no-name-in-module return encodestring(data) From cbe5ca177341a624e5536056a7e393ea930f15cd Mon Sep 17 00:00:00 2001 From: Ryan K Date: Wed, 21 Oct 2020 14:55:18 -0700 Subject: [PATCH 128/179] ADT GA SDK update (#262) * ADT GA Release Update (#10) * Regenerated SDK from 2020-10-31 swagger * Added support for --deadletter-sas-uri in dt route create commands * Removed preview tag from az dt command group * Updated python minimum requirement to 3.6 * Added documentation to explain that --properties are required for dt twin create if the twin contains components * Minor test updates --- azext_iot/digitaltwins/_help.py | 28 +- azext_iot/digitaltwins/command_map.py | 1 - azext_iot/digitaltwins/commands_resource.py | 12 + azext_iot/digitaltwins/params.py | 12 +- azext_iot/digitaltwins/providers/__init__.py | 4 +- azext_iot/digitaltwins/providers/base.py | 4 +- azext_iot/digitaltwins/providers/model.py | 2 +- azext_iot/digitaltwins/providers/resource.py | 76 +-- azext_iot/digitaltwins/providers/route.py | 4 +- azext_iot/digitaltwins/providers/twin.py | 8 +- azext_iot/sdk/digitaltwins/__init__.py | 19 +- .../controlplane}/__init__.py | 0 .../azure_digital_twins_management_client.py | 17 +- .../controlplane}/models/__init__.py | 18 - ...e_digital_twins_management_client_enums.py | 20 +- .../models/check_name_request.py | 0 .../models/check_name_request_py3.py | 0 .../controlplane}/models/check_name_result.py | 6 +- .../models/check_name_result_py3.py | 8 +- .../models/digital_twins_description.py | 10 +- .../models/digital_twins_description_paged.py | 2 +- .../models/digital_twins_description_py3.py | 10 +- .../digital_twins_endpoint_resource.py} | 23 +- .../digital_twins_endpoint_resource_paged.py | 5 +- ...ital_twins_endpoint_resource_properties.py | 14 +- ..._twins_endpoint_resource_properties_py3.py | 16 +- .../digital_twins_endpoint_resource_py3.py} | 25 +- .../models/digital_twins_patch_description.py | 0 .../digital_twins_patch_description_py3.py | 0 .../models/digital_twins_resource.py | 3 +- .../models/digital_twins_resource_py3.py | 8 +- .../controlplane}/models/error_definition.py | 2 +- .../models/error_definition_py3.py | 2 +- .../controlplane}/models/error_response.py | 2 +- .../models/error_response_py3.py | 2 +- .../controlplane}/models/event_grid.py | 24 +- .../controlplane}/models/event_grid_py3.py | 28 +- .../controlplane}/models/event_hub.py | 25 +- .../controlplane}/models/event_hub_py3.py | 29 +- .../controlplane}/models/external_resource.py | 2 +- .../models/external_resource_py3.py | 2 +- .../controlplane}/models/operation.py | 13 +- .../controlplane}/models/operation_display.py | 0 .../models/operation_display_py3.py | 0 .../controlplane}/models/operation_paged.py | 2 +- .../controlplane}/models/operation_py3.py | 13 +- .../controlplane}/models/service_bus.py | 21 +- .../controlplane}/models/service_bus_py3.py | 25 +- .../controlplane}/operations/__init__.py | 0 .../digital_twins_endpoint_operations.py | 66 +- .../operations/digital_twins_operations.py | 126 ++-- .../controlplane}/operations/operations.py | 8 +- .../{ => controlplane}/version.py | 2 +- .../sdk/digitaltwins/dataplane/__init__.py | 18 + .../azure_digital_twins_api.py | 2 +- .../digitaltwins/dataplane/models/__init__.py | 120 ++++ .../models/digital_twin_models_add_options.py | 34 ++ .../digital_twin_models_add_options_py3.py | 34 ++ .../digital_twin_models_delete_options.py | 34 ++ .../digital_twin_models_delete_options_py3.py | 34 ++ .../digital_twin_models_get_by_id_options.py | 34 ++ ...gital_twin_models_get_by_id_options_py3.py | 34 ++ .../digital_twin_models_list_options.py | 21 +- .../digital_twin_models_list_options_py3.py | 39 ++ .../digital_twin_models_update_options.py | 34 ++ .../digital_twin_models_update_options_py3.py | 34 ++ .../models/digital_twins_add_options.py | 39 ++ .../models/digital_twins_add_options_py3.py | 39 ++ .../digital_twins_add_relationship_options.py | 39 ++ ...ital_twins_add_relationship_options_py3.py | 39 ++ .../models/digital_twins_delete_options.py | 39 ++ .../digital_twins_delete_options_py3.py | 39 ++ ...gital_twins_delete_relationship_options.py | 39 ++ ...l_twins_delete_relationship_options_py3.py | 39 ++ .../models/digital_twins_get_by_id_options.py | 34 ++ .../digital_twins_get_by_id_options_py3.py | 34 ++ .../digital_twins_get_component_options.py | 34 ++ ...digital_twins_get_component_options_py3.py | 34 ++ ...al_twins_get_relationship_by_id_options.py | 34 ++ ...wins_get_relationship_by_id_options_py3.py | 34 ++ ...ins_list_incoming_relationships_options.py | 34 ++ ...list_incoming_relationships_options_py3.py | 34 ++ ...igital_twins_list_relationships_options.py | 34 ++ ...al_twins_list_relationships_options_py3.py | 34 ++ .../models/digital_twins_model_data.py} | 12 +- .../models/digital_twins_model_data_paged.py} | 8 +- .../models/digital_twins_model_data_py3.py} | 12 +- ..._twins_send_component_telemetry_options.py | 34 ++ ...ns_send_component_telemetry_options_py3.py | 34 ++ .../digital_twins_send_telemetry_options.py | 34 ++ ...igital_twins_send_telemetry_options_py3.py | 34 ++ .../digital_twins_update_component_options.py | 39 ++ ...ital_twins_update_component_options_py3.py | 39 ++ .../models/digital_twins_update_options.py | 39 ++ .../digital_twins_update_options_py3.py | 39 ++ ...gital_twins_update_relationship_options.py | 39 ++ ...l_twins_update_relationship_options_py3.py | 39 ++ .../{ => dataplane}/models/error.py | 0 .../{ => dataplane}/models/error_py3.py | 0 .../{ => dataplane}/models/error_response.py | 0 .../models/error_response_py3.py | 0 .../{ => dataplane}/models/event_route.py | 12 +- .../models/event_route_paged.py | 0 .../{ => dataplane}/models/event_route_py3.py | 14 +- .../models/event_routes_add_options.py | 34 ++ .../models/event_routes_add_options_py3.py | 34 ++ .../models/event_routes_delete_options.py | 34 ++ .../models/event_routes_delete_options_py3.py | 34 ++ .../models/event_routes_get_by_id_options.py | 34 ++ .../event_routes_get_by_id_options_py3.py | 34 ++ .../models/event_routes_list_options.py | 21 +- .../models/event_routes_list_options_py3.py | 39 ++ .../models/incoming_relationship.py | 0 .../models/incoming_relationship_paged.py | 0 .../models/incoming_relationship_py3.py | 0 .../{ => dataplane}/models/inner_error.py | 0 .../{ => dataplane}/models/inner_error_py3.py | 0 .../{ => dataplane}/models/object_paged.py | 0 .../models/query_query_twins_options.py | 39 ++ .../models/query_query_twins_options_py3.py | 39 ++ .../{ => dataplane}/models/query_result.py | 8 +- .../models/query_result_py3.py | 10 +- .../models/query_specification.py | 0 .../models/query_specification_py3.py | 0 .../{ => dataplane}/operations/__init__.py | 0 .../digital_twin_models_operations.py | 169 +++-- .../operations/digital_twins_operations.py | 578 +++++++++++++----- .../operations/event_routes_operations.py | 106 +++- .../operations/query_operations.py | 35 +- .../dataplane}/version.py | 3 +- azext_iot/sdk/digitaltwins/models/__init__.py | 54 -- .../digital_twin_models_list_options_py3.py | 30 - .../models/event_routes_list_options_py3.py | 30 - .../digitaltwins/models/model_data_paged.py | 28 - .../models/digital_twins_endpoint_resource.py | 59 -- .../digital_twins_endpoint_resource_py3.py | 59 -- .../models/digital_twins_sku_info.py | 35 -- .../models/digital_twins_sku_info_py3.py | 35 -- .../models/integration_resource.py | 61 -- .../models/integration_resource_py3.py | 61 -- .../models/integration_resource_state1.py | 38 -- .../models/integration_resource_state1_py3.py | 38 -- azext_iot/tests/digitaltwins/__init__.py | 1 + .../tests/digitaltwins/models/Floor.json | 4 +- .../digitaltwins/models/nested/Room.json | 2 +- .../test_dt_model_lifecycle_int.py | 4 +- .../digitaltwins/test_dt_provider_unit.py | 2 +- .../test_dt_resource_lifecycle_int.py | 49 +- .../test_dt_twin_lifecycle_int.py | 61 +- setup.py | 2 +- 150 files changed, 2981 insertions(+), 1224 deletions(-) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/__init__.py (100%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/azure_digital_twins_management_client.py (83%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/__init__.py (76%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/azure_digital_twins_management_client_enums.py (76%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/check_name_request.py (100%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/check_name_request_py3.py (100%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/check_name_result.py (87%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/check_name_result_py3.py (81%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_description.py (92%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_description_paged.py (91%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_description_py3.py (93%) rename azext_iot/sdk/{digitaltwins_arm/models/integration_resource_update_info.py => digitaltwins/controlplane/models/digital_twins_endpoint_resource.py} (65%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_endpoint_resource_paged.py (79%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_endpoint_resource_properties.py (83%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_endpoint_resource_properties_py3.py (81%) rename azext_iot/sdk/{digitaltwins_arm/models/integration_resource_update_info_py3.py => digitaltwins/controlplane/models/digital_twins_endpoint_resource_py3.py} (64%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_patch_description.py (100%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_patch_description_py3.py (100%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_resource.py (92%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/digital_twins_resource_py3.py (88%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/error_definition.py (94%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/error_definition_py3.py (94%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/error_response.py (95%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/error_response_py3.py (95%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/event_grid.py (78%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/event_grid_py3.py (72%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/event_hub.py (78%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/event_hub_py3.py (71%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/external_resource.py (97%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/external_resource_py3.py (97%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/operation.py (69%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/operation_display.py (100%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/operation_display_py3.py (100%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/operation_paged.py (93%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/operation_py3.py (69%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/service_bus.py (80%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/models/service_bus_py3.py (73%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/operations/__init__.py (100%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/operations/digital_twins_endpoint_operations.py (85%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/operations/digital_twins_operations.py (87%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/controlplane}/operations/operations.py (92%) rename azext_iot/sdk/digitaltwins/{ => controlplane}/version.py (93%) create mode 100644 azext_iot/sdk/digitaltwins/dataplane/__init__.py rename azext_iot/sdk/digitaltwins/{ => dataplane}/azure_digital_twins_api.py (98%) create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/__init__.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_add_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_add_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_delete_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_delete_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_get_by_id_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_get_by_id_options_py3.py rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/digital_twin_models_list_options.py (52%) create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_list_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_update_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_update_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_relationship_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_relationship_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_relationship_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_relationship_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_by_id_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_by_id_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_component_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_component_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_relationship_by_id_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_relationship_by_id_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_incoming_relationships_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_incoming_relationships_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_relationships_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_relationships_options_py3.py rename azext_iot/sdk/digitaltwins/{models/model_data.py => dataplane/models/digital_twins_model_data.py} (87%) rename azext_iot/sdk/{digitaltwins_arm/models/integration_resource_paged.py => digitaltwins/dataplane/models/digital_twins_model_data_paged.py} (67%) rename azext_iot/sdk/digitaltwins/{models/model_data_py3.py => dataplane/models/digital_twins_model_data_py3.py} (87%) create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_component_telemetry_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_component_telemetry_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_telemetry_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_telemetry_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_component_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_component_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_relationship_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_relationship_options_py3.py rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/error.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/error_py3.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/error_response.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/error_response_py3.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/event_route.py (83%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/event_route_paged.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/event_route_py3.py (79%) create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/event_routes_add_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/event_routes_add_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/event_routes_delete_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/event_routes_delete_options_py3.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/event_routes_get_by_id_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/event_routes_get_by_id_options_py3.py rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/event_routes_list_options.py (52%) create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/event_routes_list_options_py3.py rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/incoming_relationship.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/incoming_relationship_paged.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/incoming_relationship_py3.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/inner_error.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/inner_error_py3.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/object_paged.py (100%) create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/query_query_twins_options.py create mode 100644 azext_iot/sdk/digitaltwins/dataplane/models/query_query_twins_options_py3.py rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/query_result.py (86%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/query_result_py3.py (82%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/query_specification.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/models/query_specification_py3.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/operations/__init__.py (100%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/operations/digital_twin_models_operations.py (65%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/operations/digital_twins_operations.py (64%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/operations/event_routes_operations.py (71%) rename azext_iot/sdk/digitaltwins/{ => dataplane}/operations/query_operations.py (70%) rename azext_iot/sdk/{digitaltwins_arm => digitaltwins/dataplane}/version.py (93%) delete mode 100644 azext_iot/sdk/digitaltwins/models/__init__.py delete mode 100644 azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options_py3.py delete mode 100644 azext_iot/sdk/digitaltwins/models/event_routes_list_options_py3.py delete mode 100644 azext_iot/sdk/digitaltwins/models/model_data_paged.py delete mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource.py delete mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_py3.py delete mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info.py delete mode 100644 azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info_py3.py delete mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource.py delete mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource_py3.py delete mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1.py delete mode 100644 azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1_py3.py diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index fc029de3a..f66740bbc 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -285,22 +285,32 @@ def load_digitaltwins_help(): helps["dt twin create"] = """ type: command short-summary: Create a digital twin on an instance. - long-summary: --properties can be inline JSON or file path. + long-summary: | + --properties can be inline JSON or file path. + Note: --properties are required for twins that contain components. examples: - name: Create a digital twin from an existing (prior-created) model. text: > - az dt twin create -n {instance_or_hostname} --dtmi dtmi:example:Room;1 + az dt twin create -n {instance_or_hostname} --dtmi "dtmi:com:example:Room;1" --twin-id {twin_id} - name: Create a digital twin from an existing (prior-created) model. Instantiate with property values. text: > - az dt twin create -n {instance_or_hostname} --dtmi dtmi:com:example:DeviceInformation;1 + az dt twin create -n {instance_or_hostname} --dtmi "dtmi:com:example:DeviceInformation;1" --twin-id {twin_id} --properties '{"manufacturer": "Microsoft"}' + - name: Create a digital twin with component from existing (prior-created) models. Instantiate component with minimum properties. + text: > + az dt twin create -n {instance_or_hostname} --dtmi "dtmi:com:example:TemperatureController;1" --twin-id {twin_id} --properties '{ + "Thermostat": { + "$metadata": {}, + } + }' + - name: Create a digital twin with component from existing (prior-created) models. Instantiate with property values. text: > - az dt twin create -n {instance_or_hostname} --dtmi dtmi:com:example:TemperatureController;1 --twin-id {twin_id} --properties '{ + az dt twin create -n {instance_or_hostname} --dtmi "dtmi:com:example:TemperatureController;1" --twin-id {twin_id} --properties '{ "Temperature": 10.2, "Thermostat": { "$metadata": {}, @@ -356,7 +366,7 @@ def load_digitaltwins_help(): - name: Query by model and project all attributes. text: > - az dt twin query -n {instance_or_hostname} -q "select * from digitaltwins T where IS_OF_MODEL(T, 'dtmi:example:Room;2')" + az dt twin query -n {instance_or_hostname} -q "select * from digitaltwins T where IS_OF_MODEL(T, 'dtmi:com:example:Room;2')" """ helps["dt twin delete"] = """ @@ -544,11 +554,11 @@ def load_digitaltwins_help(): examples: - name: Show model meta data text: > - az dt model show -n {instance_or_hostname} --dtmi "dtmi:example:Floor;1" + az dt model show -n {instance_or_hostname} --dtmi "dtmi:com:example:Floor;1" - name: Show model meta data and definition text: > - az dt model show -n {instance_or_hostname} --dtmi "dtmi:example:Floor;1" --definition + az dt model show -n {instance_or_hostname} --dtmi "dtmi:com:example:Floor;1" --definition """ helps["dt model list"] = """ @@ -576,7 +586,7 @@ def load_digitaltwins_help(): examples: - name: Decommision a target model text: > - az dt model update -n {instance_or_hostname} --dtmi "dtmi:example:Floor;1" --decommission + az dt model update -n {instance_or_hostname} --dtmi "dtmi:com:example:Floor;1" --decommission """ helps["dt model delete"] = """ @@ -586,5 +596,5 @@ def load_digitaltwins_help(): examples: - name: Delete a target model. text: > - az dt model delete -n {instance_or_hostname} --dtmi "dtmi:example:Floor;1" + az dt model delete -n {instance_or_hostname} --dtmi "dtmi:com:example:Floor;1" """ diff --git a/azext_iot/digitaltwins/command_map.py b/azext_iot/digitaltwins/command_map.py index 99722f0f3..c2cc25020 100644 --- a/azext_iot/digitaltwins/command_map.py +++ b/azext_iot/digitaltwins/command_map.py @@ -40,7 +40,6 @@ def load_digitaltwins_commands(self, _): "dt", command_type=digitaltwins_resource_ops, resource_type=ResourceType.MGMT_RESOURCE_RESOURCES, - is_preview=True, ) as cmd_group: cmd_group.command("create", "create_instance") cmd_group.show_command("show", "show_instance") diff --git a/azext_iot/digitaltwins/commands_resource.py b/azext_iot/digitaltwins/commands_resource.py index 206e00d91..aa1b0b58b 100644 --- a/azext_iot/digitaltwins/commands_resource.py +++ b/azext_iot/digitaltwins/commands_resource.py @@ -63,6 +63,7 @@ def add_endpoint_eventgrid( eventgrid_resource_group, resource_group_name=None, endpoint_subscription=None, + dead_letter_endpoint=None, tags=None, ): return _add_endpoint_eventgrid( @@ -73,6 +74,7 @@ def add_endpoint_eventgrid( eventgrid_topic_name=eventgrid_topic_name, resource_group_name=resource_group_name, endpoint_subscription=endpoint_subscription, + dead_letter_endpoint=dead_letter_endpoint, tags=tags, ) @@ -85,6 +87,7 @@ def _add_endpoint_eventgrid( eventgrid_resource_group, resource_group_name=None, endpoint_subscription=None, + dead_letter_endpoint=None, tags=None, ): rp = ResourceProvider(cmd) @@ -96,6 +99,7 @@ def _add_endpoint_eventgrid( endpoint_resource_name=eventgrid_topic_name, endpoint_resource_group=eventgrid_resource_group, endpoint_subscription=endpoint_subscription, + dead_letter_endpoint=dead_letter_endpoint, tags=tags, ) @@ -110,6 +114,7 @@ def add_endpoint_servicebus( servicebus_namespace, resource_group_name=None, endpoint_subscription=None, + dead_letter_endpoint=None, tags=None, ): return _add_endpoint_servicebus( @@ -122,6 +127,7 @@ def add_endpoint_servicebus( servicebus_namespace=servicebus_namespace, resource_group_name=resource_group_name, endpoint_subscription=endpoint_subscription, + dead_letter_endpoint=dead_letter_endpoint, tags=tags, ) @@ -136,6 +142,7 @@ def _add_endpoint_servicebus( servicebus_namespace, resource_group_name=None, endpoint_subscription=None, + dead_letter_endpoint=None, tags=None, ): rp = ResourceProvider(cmd) @@ -149,6 +156,7 @@ def _add_endpoint_servicebus( endpoint_resource_namespace=servicebus_namespace, endpoint_resource_policy=servicebus_policy, endpoint_subscription=endpoint_subscription, + dead_letter_endpoint=dead_letter_endpoint, tags=tags, ) @@ -163,6 +171,7 @@ def add_endpoint_eventhub( eventhub_namespace, resource_group_name=None, endpoint_subscription=None, + dead_letter_endpoint=None, tags=None, ): return _add_endpoint_eventhub( @@ -175,6 +184,7 @@ def add_endpoint_eventhub( eventhub_namespace=eventhub_namespace, resource_group_name=resource_group_name, endpoint_subscription=endpoint_subscription, + dead_letter_endpoint=dead_letter_endpoint, tags=tags, ) @@ -189,6 +199,7 @@ def _add_endpoint_eventhub( eventhub_namespace, resource_group_name=None, endpoint_subscription=None, + dead_letter_endpoint=None, tags=None, ): rp = ResourceProvider(cmd) @@ -202,5 +213,6 @@ def _add_endpoint_eventhub( endpoint_resource_namespace=eventhub_namespace, endpoint_resource_policy=eventhub_policy, endpoint_subscription=endpoint_subscription, + dead_letter_endpoint=dead_letter_endpoint, tags=tags, ) diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index 45600ea17..6c28305bb 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -82,7 +82,7 @@ def load_digitaltwins_arguments(self, _): context.argument( "model_id", options_list=["--model-id", "--dtmi", "-m"], - help="Digital Twins model Id. Example: dtmi:example:Room;2", + help="Digital Twins model Id. Example: dtmi:com:example:Room;2", ) context.argument( "twin_id", options_list=["--twin-id", "-t"], help="The digital twin Id.", @@ -102,9 +102,10 @@ def load_digitaltwins_arguments(self, _): with self.argument_context("dt endpoint create") as context: context.argument( - "tags", - options_list=["--tags"], - help="Digital Twins endpoint tags. Property bag in key-value pairs with the following format: a=b;c=d.", + "dead_letter_endpoint", + options_list=["--deadletter-sas-uri", "--dsu"], + help="Dead-letter storage container URL with SAS token", + arg_group="Dead-letter Endpoint" ) with self.argument_context("dt endpoint create eventgrid") as context: @@ -234,7 +235,8 @@ def load_digitaltwins_arguments(self, _): "properties", options_list=["--properties", "-p"], help="Initial property values for instantiating a digital twin or related components. " - "Provide file path or inline JSON.", + "Provide file path or inline JSON. Properties are required for twins that contain components, " + "at the minimum you must provide an empty $metadata object for each component.", ) with self.argument_context("dt twin telemetry") as context: diff --git a/azext_iot/digitaltwins/providers/__init__.py b/azext_iot/digitaltwins/providers/__init__.py index f494a0333..274d3cfc4 100644 --- a/azext_iot/digitaltwins/providers/__init__.py +++ b/azext_iot/digitaltwins/providers/__init__.py @@ -4,8 +4,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_iot.sdk.digitaltwins_arm import AzureDigitalTwinsManagementClient -from azext_iot.sdk.digitaltwins_arm.models import ErrorResponseException +from azext_iot.sdk.digitaltwins.controlplane import AzureDigitalTwinsManagementClient +from azext_iot.sdk.digitaltwins.controlplane.models import ErrorResponseException from msrestazure.azure_exceptions import CloudError __all__ = [ diff --git a/azext_iot/digitaltwins/providers/base.py b/azext_iot/digitaltwins/providers/base.py index e7d21a40e..c93d94aad 100644 --- a/azext_iot/digitaltwins/providers/base.py +++ b/azext_iot/digitaltwins/providers/base.py @@ -6,8 +6,8 @@ from azext_iot.digitaltwins.providers.resource import ResourceProvider from azext_iot.digitaltwins.providers.auth import DigitalTwinAuthentication -from azext_iot.sdk.digitaltwins import AzureDigitalTwinsAPI -from azext_iot.sdk.digitaltwins.models import ErrorResponseException +from azext_iot.sdk.digitaltwins.dataplane import AzureDigitalTwinsAPI +from azext_iot.sdk.digitaltwins.dataplane.models import ErrorResponseException from azext_iot.constants import DIGITALTWINS_RESOURCE_ID from azext_iot.common.utility import valid_hostname from knack.cli import CLIError diff --git a/azext_iot/digitaltwins/providers/model.py b/azext_iot/digitaltwins/providers/model.py index f7b2a9ec0..2adbb6fc3 100644 --- a/azext_iot/digitaltwins/providers/model.py +++ b/azext_iot/digitaltwins/providers/model.py @@ -84,7 +84,7 @@ def get(self, id, get_definition=False): def list( self, get_definition=False, dependencies_for=None, top=None ): # top is guarded for int() in arg def - from azext_iot.sdk.digitaltwins.models import DigitalTwinModelsListOptions + from azext_iot.sdk.digitaltwins.dataplane.models import DigitalTwinModelsListOptions list_options = DigitalTwinModelsListOptions(max_item_count=top) diff --git a/azext_iot/digitaltwins/providers/resource.py b/azext_iot/digitaltwins/providers/resource.py index 842111dc5..dfebc3617 100644 --- a/azext_iot/digitaltwins/providers/resource.py +++ b/azext_iot/digitaltwins/providers/resource.py @@ -10,6 +10,11 @@ ErrorResponseException, ) from azext_iot.digitaltwins.providers.rbac import RbacProvider +from azext_iot.sdk.digitaltwins.controlplane.models import ( + EventGrid as EventGridEndpointProperties, + EventHub as EventHubEndpointProperties, + ServiceBus as ServiceBusEndpointProperties, +) from azext_iot.common.utility import validate_key_value_pairs, unpack_msrest_error from knack.util import CLIError @@ -20,13 +25,18 @@ def __init__(self, cmd): self.mgmt_sdk = self.get_mgmt_sdk() self.rbac = RbacProvider() - def create(self, name, resource_group_name, location=None, tags=None, timeout=20): + def create(self, name, resource_group_name, location=None, tags=None, timeout=30): if tags: tags = validate_key_value_pairs(tags) if not location: from azext_iot.common.embedded_cli import EmbeddedCLI - resource_group_meta = EmbeddedCLI().invoke("group show --name {}".format(resource_group_name)).as_json() + + resource_group_meta = ( + EmbeddedCLI() + .invoke("group show --name {}".format(resource_group_name)) + .as_json() + ) location = resource_group_meta["location"] try: @@ -111,12 +121,10 @@ def delete(self, name, resource_group_name=None): if not resource_group_name: resource_group_name = self.get_rg(target_instance) - # TODO: Don't return result. Issue in service related to status code and polling. try: - self.mgmt_sdk.digital_twins.delete( + return self.mgmt_sdk.digital_twins.delete( resource_name=name, resource_group_name=resource_group_name, - polling=False, ) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) @@ -202,12 +210,10 @@ def delete_endpoint(self, name, endpoint_name, resource_group_name=None): resource_group_name = self.get_rg(target_instance) try: - # TODO: Polling set to false return self.mgmt_sdk.digital_twins_endpoint.delete( resource_name=target_instance.name, endpoint_name=endpoint_name, resource_group_name=resource_group_name, - polling=False, ) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) @@ -223,6 +229,7 @@ def add_endpoint( endpoint_resource_policy=None, endpoint_resource_namespace=None, endpoint_subscription=None, + dead_letter_endpoint=None, tags=None, resource_group_name=None, timeout=20, @@ -246,24 +253,23 @@ def add_endpoint( ) ) - if tags: - tags = validate_key_value_pairs(tags) - target_instance = self.find_instance( name=name, resource_group_name=resource_group_name ) if not resource_group_name: resource_group_name = self.get_rg(target_instance) - payload = {"tags": tags} cli = EmbeddedCLI() error_prefix = "Could not create ADT instance endpoint. Unable to retrieve" + + properties = {} + if endpoint_resource_type == ADTEndpointType.eventgridtopic: eg_topic_keys_op = cli.invoke( "eventgrid topic key list -n {} -g {}".format( endpoint_resource_name, endpoint_resource_group ), - subscription=endpoint_subscription + subscription=endpoint_subscription, ) if not eg_topic_keys_op.success(): raise CLIError("{} Event Grid topic keys.".format(error_prefix)) @@ -273,16 +279,18 @@ def add_endpoint( "eventgrid topic show -n {} -g {}".format( endpoint_resource_name, endpoint_resource_group ), - subscription=endpoint_subscription + subscription=endpoint_subscription, ) if not eg_topic_endpoint_op.success(): raise CLIError("{} Event Grid topic endpoint.".format(error_prefix)) eg_topic_endpoint = eg_topic_endpoint_op.as_json() - payload["endpointType"] = "EventGrid" - payload["accessKey1"] = eg_topic_keys["key1"] - payload["accessKey2"] = eg_topic_keys["key2"] - payload["TopicEndpoint"] = eg_topic_endpoint["endpoint"] + properties = EventGridEndpointProperties( + access_key1=eg_topic_keys["key1"], + access_key2=eg_topic_keys["key2"], + dead_letter_secret=dead_letter_endpoint, + topic_endpoint=eg_topic_endpoint["endpoint"], + ) elif endpoint_resource_type == ADTEndpointType.servicebus: sb_topic_keys_op = cli.invoke( @@ -293,19 +301,17 @@ def add_endpoint( endpoint_resource_group, endpoint_resource_name, ), - subscription=endpoint_subscription + subscription=endpoint_subscription, ) if not sb_topic_keys_op.success(): raise CLIError("{} Service Bus topic keys.".format(error_prefix)) sb_topic_keys = sb_topic_keys_op.as_json() - payload["endpointType"] = "ServiceBus" - payload["primaryConnectionString"] = sb_topic_keys[ - "primaryConnectionString" - ] - payload["secondaryConnectionString"] = sb_topic_keys[ - "secondaryConnectionString" - ] + properties = ServiceBusEndpointProperties( + primary_connection_string=sb_topic_keys["primaryConnectionString"], + secondary_connection_string=sb_topic_keys["secondaryConnectionString"], + dead_letter_secret=dead_letter_endpoint, + ) elif endpoint_resource_type == ADTEndpointType.eventhub: eventhub_topic_keys_op = cli.invoke( @@ -316,21 +322,21 @@ def add_endpoint( endpoint_resource_group, endpoint_resource_name, ), - subscription=endpoint_subscription + subscription=endpoint_subscription, ) if not eventhub_topic_keys_op.success(): raise CLIError("{} Event Hub keys.".format(error_prefix)) eventhub_topic_keys = eventhub_topic_keys_op.as_json() - payload["endpointType"] = "EventHub" - payload["connectionString-PrimaryKey"] = eventhub_topic_keys[ - "primaryConnectionString" - ] - payload["connectionString-SecondaryKey"] = eventhub_topic_keys[ - "secondaryConnectionString" - ] - - properties = {"properties": payload} + properties = EventHubEndpointProperties( + connection_string_primary_key=eventhub_topic_keys[ + "primaryConnectionString" + ], + connection_string_secondary_key=eventhub_topic_keys[ + "secondaryConnectionString" + ], + dead_letter_secret=dead_letter_endpoint, + ) try: return self.mgmt_sdk.digital_twins_endpoint.create_or_update( diff --git a/azext_iot/digitaltwins/providers/route.py b/azext_iot/digitaltwins/providers/route.py index 88968ad75..131c98e37 100644 --- a/azext_iot/digitaltwins/providers/route.py +++ b/azext_iot/digitaltwins/providers/route.py @@ -25,7 +25,7 @@ def get(self, route_name): raise CLIError(unpack_msrest_error(e)) def list(self, top=None): # top is guarded for int() in arg def - from azext_iot.sdk.digitaltwins.models import EventRoutesListOptions + from azext_iot.sdk.digitaltwins.dataplane.models import EventRoutesListOptions list_options = EventRoutesListOptions(max_item_count=top) @@ -40,7 +40,7 @@ def create(self, route_name, endpoint_name, filter=None): # TODO: Adding routes does not return an object try: - self.sdk.add(id=route_name, endpoint_id=endpoint_name, filter=filter) + self.sdk.add(id=route_name, endpoint_name=endpoint_name, filter=filter) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/digitaltwins/providers/twin.py b/azext_iot/digitaltwins/providers/twin.py index 821ac214c..8e36aa2f3 100644 --- a/azext_iot/digitaltwins/providers/twin.py +++ b/azext_iot/digitaltwins/providers/twin.py @@ -31,7 +31,7 @@ def invoke_query(self, query, show_cost): accumulated_result, cost = accumulate_result( self.query_sdk.query_twins, - values_name="items", + values_name="value", token_name="continuationToken", token_arg_name="continuation_token", query=query, @@ -48,7 +48,7 @@ def create(self, twin_id, model_id, properties=None): target_model = self.model_provider.get(id=model_id) twin_request = { "$dtId": twin_id, - "$metadata": {"$model": target_model["id"], "$kind": "DigitalTwin"}, + "$metadata": {"$model": target_model["id"]}, } if properties: @@ -237,7 +237,7 @@ def send_telemetry(self, twin_id, telemetry=None, dt_id=None, component_path=Non if component_path: self.twins_sdk.send_component_telemetry( id=twin_id, - dt_id=dt_id, + message_id=dt_id, dt_timestamp=dt_timestamp, component_path=component_path, telemetry=telemetry_request, @@ -245,7 +245,7 @@ def send_telemetry(self, twin_id, telemetry=None, dt_id=None, component_path=Non self.twins_sdk.send_telemetry( id=twin_id, - dt_id=dt_id, + message_id=dt_id, dt_timestamp=dt_timestamp, telemetry=telemetry_request, ) diff --git a/azext_iot/sdk/digitaltwins/__init__.py b/azext_iot/sdk/digitaltwins/__init__.py index bd6770159..55614acbf 100644 --- a/azext_iot/sdk/digitaltwins/__init__.py +++ b/azext_iot/sdk/digitaltwins/__init__.py @@ -1,18 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .azure_digital_twins_api import AzureDigitalTwinsAPI -from .version import VERSION - -__all__ = ['AzureDigitalTwinsAPI'] - -__version__ = VERSION - +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/azext_iot/sdk/digitaltwins_arm/__init__.py b/azext_iot/sdk/digitaltwins/controlplane/__init__.py similarity index 100% rename from azext_iot/sdk/digitaltwins_arm/__init__.py rename to azext_iot/sdk/digitaltwins/controlplane/__init__.py diff --git a/azext_iot/sdk/digitaltwins_arm/azure_digital_twins_management_client.py b/azext_iot/sdk/digitaltwins/controlplane/azure_digital_twins_management_client.py similarity index 83% rename from azext_iot/sdk/digitaltwins_arm/azure_digital_twins_management_client.py rename to azext_iot/sdk/digitaltwins/controlplane/azure_digital_twins_management_client.py index bf2c85f4a..1f6f0e312 100644 --- a/azext_iot/sdk/digitaltwins_arm/azure_digital_twins_management_client.py +++ b/azext_iot/sdk/digitaltwins/controlplane/azure_digital_twins_management_client.py @@ -43,12 +43,9 @@ def __init__( if not base_url: base_url = 'https://management.azure.com' - # @digimaun - use azmock server - # base_url = 'http://127.0.0.1:8005' - super(AzureDigitalTwinsManagementClientConfiguration, self).__init__(base_url) - self.add_user_agent('azuredigitaltwinsmanagementclient/{}'.format(VERSION)) + self.add_user_agent('azure-mgmt-digitaltwins/{}'.format(VERSION)) self.add_user_agent(USER_AGENT) self.credentials = credentials @@ -62,15 +59,11 @@ class AzureDigitalTwinsManagementClient(SDKClient): :vartype config: AzureDigitalTwinsManagementClientConfiguration :ivar digital_twins: DigitalTwins operations - :vartype digital_twins: digitaltwins-arm.operations.DigitalTwinsOperations + :vartype digital_twins: azure.mgmt.digitaltwins.operations.DigitalTwinsOperations :ivar digital_twins_endpoint: DigitalTwinsEndpoint operations - :vartype digital_twins_endpoint: digitaltwins-arm.operations.DigitalTwinsEndpointOperations - :ivar io_thub: IoTHub operations - :vartype io_thub: digitaltwins-arm.operations.IoTHubOperations - :ivar digital_twins_io_thubs: DigitalTwinsIoTHubs operations - :vartype digital_twins_io_thubs: digitaltwins-arm.operations.DigitalTwinsIoTHubsOperations + :vartype digital_twins_endpoint: azure.mgmt.digitaltwins.operations.DigitalTwinsEndpointOperations :ivar operations: Operations operations - :vartype operations: digitaltwins-arm.operations.Operations + :vartype operations: azure.mgmt.digitaltwins.operations.Operations :param credentials: Credentials needed for the client to connect to Azure. :type credentials: :mod:`A msrestazure Credentials @@ -87,7 +80,7 @@ def __init__( super(AzureDigitalTwinsManagementClient, self).__init__(self.config.credentials, self.config) client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2020-03-01-preview' + self.api_version = '2020-10-31' self._serialize = Serializer(client_models) self._deserialize = Deserializer(client_models) diff --git a/azext_iot/sdk/digitaltwins_arm/models/__init__.py b/azext_iot/sdk/digitaltwins/controlplane/models/__init__.py similarity index 76% rename from azext_iot/sdk/digitaltwins_arm/models/__init__.py rename to azext_iot/sdk/digitaltwins/controlplane/models/__init__.py index e572c8103..92d8cf33c 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/__init__.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/__init__.py @@ -10,7 +10,6 @@ # -------------------------------------------------------------------------- try: - from .digital_twins_sku_info_py3 import DigitalTwinsSkuInfo from .digital_twins_description_py3 import DigitalTwinsDescription from .digital_twins_patch_description_py3 import DigitalTwinsPatchDescription from .digital_twins_resource_py3 import DigitalTwinsResource @@ -21,17 +20,12 @@ from .check_name_request_py3 import CheckNameRequest from .check_name_result_py3 import CheckNameResult from .external_resource_py3 import ExternalResource - from .integration_resource_py3 import IntegrationResource - from .integration_resource_update_info_py3 import IntegrationResourceUpdateInfo - from .integration_resource_state1_py3 import IntegrationResourceState1 from .digital_twins_endpoint_resource_properties_py3 import DigitalTwinsEndpointResourceProperties from .digital_twins_endpoint_resource_py3 import DigitalTwinsEndpointResource from .service_bus_py3 import ServiceBus from .event_hub_py3 import EventHub from .event_grid_py3 import EventGrid - from .digital_twins_description_list_custom import DigitalTwinsDescriptionListCustom except (SyntaxError, ImportError): - from .digital_twins_sku_info import DigitalTwinsSkuInfo from .digital_twins_description import DigitalTwinsDescription from .digital_twins_patch_description import DigitalTwinsPatchDescription from .digital_twins_resource import DigitalTwinsResource @@ -42,9 +36,6 @@ from .check_name_request import CheckNameRequest from .check_name_result import CheckNameResult from .external_resource import ExternalResource - from .integration_resource import IntegrationResource - from .integration_resource_update_info import IntegrationResourceUpdateInfo - from .integration_resource_state1 import IntegrationResourceState1 from .digital_twins_endpoint_resource_properties import DigitalTwinsEndpointResourceProperties from .digital_twins_endpoint_resource import DigitalTwinsEndpointResource from .service_bus import ServiceBus @@ -52,17 +43,14 @@ from .event_grid import EventGrid from .digital_twins_description_paged import DigitalTwinsDescriptionPaged from .digital_twins_endpoint_resource_paged import DigitalTwinsEndpointResourcePaged -from .integration_resource_paged import IntegrationResourcePaged from .operation_paged import OperationPaged from .azure_digital_twins_management_client_enums import ( ProvisioningState, Reason, - IntegrationResourceState, EndpointProvisioningState, ) __all__ = [ - 'DigitalTwinsSkuInfo', 'DigitalTwinsDescription', 'DigitalTwinsPatchDescription', 'DigitalTwinsResource', @@ -73,9 +61,6 @@ 'CheckNameRequest', 'CheckNameResult', 'ExternalResource', - 'IntegrationResource', - 'IntegrationResourceUpdateInfo', - 'IntegrationResourceState1', 'DigitalTwinsEndpointResourceProperties', 'DigitalTwinsEndpointResource', 'ServiceBus', @@ -83,11 +68,8 @@ 'EventGrid', 'DigitalTwinsDescriptionPaged', 'DigitalTwinsEndpointResourcePaged', - 'IntegrationResourcePaged', 'OperationPaged', 'ProvisioningState', 'Reason', - 'IntegrationResourceState', 'EndpointProvisioningState', - 'DigitalTwinsDescriptionListCustom' ] diff --git a/azext_iot/sdk/digitaltwins_arm/models/azure_digital_twins_management_client_enums.py b/azext_iot/sdk/digitaltwins/controlplane/models/azure_digital_twins_management_client_enums.py similarity index 76% rename from azext_iot/sdk/digitaltwins_arm/models/azure_digital_twins_management_client_enums.py rename to azext_iot/sdk/digitaltwins/controlplane/models/azure_digital_twins_management_client_enums.py index 5d4e0a7b3..ff6d2b607 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/azure_digital_twins_management_client_enums.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/azure_digital_twins_management_client_enums.py @@ -19,6 +19,11 @@ class ProvisioningState(str, Enum): succeeded = "Succeeded" failed = "Failed" canceled = "Canceled" + deleted = "Deleted" + warning = "Warning" + suspending = "Suspending" + restoring = "Restoring" + moving = "Moving" class Reason(str, Enum): @@ -27,15 +32,6 @@ class Reason(str, Enum): already_exists = "AlreadyExists" -class IntegrationResourceState(str, Enum): - - provisioning = "Provisioning" - deleting = "Deleting" - succeeded = "Succeeded" - failed = "Failed" - canceled = "Canceled" - - class EndpointProvisioningState(str, Enum): provisioning = "Provisioning" @@ -43,3 +39,9 @@ class EndpointProvisioningState(str, Enum): succeeded = "Succeeded" failed = "Failed" canceled = "Canceled" + deleted = "Deleted" + warning = "Warning" + suspending = "Suspending" + restoring = "Restoring" + moving = "Moving" + disabled = "Disabled" diff --git a/azext_iot/sdk/digitaltwins_arm/models/check_name_request.py b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_request.py similarity index 100% rename from azext_iot/sdk/digitaltwins_arm/models/check_name_request.py rename to azext_iot/sdk/digitaltwins/controlplane/models/check_name_request.py diff --git a/azext_iot/sdk/digitaltwins_arm/models/check_name_request_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_request_py3.py similarity index 100% rename from azext_iot/sdk/digitaltwins_arm/models/check_name_request_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/check_name_request_py3.py diff --git a/azext_iot/sdk/digitaltwins_arm/models/check_name_result.py b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result.py similarity index 87% rename from azext_iot/sdk/digitaltwins_arm/models/check_name_result.py rename to azext_iot/sdk/digitaltwins/controlplane/models/check_name_result.py index 9e7a1ef67..5fddb0bfe 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/check_name_result.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result.py @@ -18,19 +18,16 @@ class CheckNameResult(Model): :param name_available: Specifies a Boolean value that indicates if the name is available. :type name_available: bool - :param name: The name that was checked. - :type name: str :param message: Message indicating an unavailable name due to a conflict, or a description of the naming rules that are violated. :type message: str :param reason: Message providing the reason why the given name is invalid. Possible values include: 'Invalid', 'AlreadyExists' - :type reason: str or ~digitaltwins-arm.models.Reason + :type reason: str or ~azure.mgmt.digitaltwins.models.Reason """ _attribute_map = { 'name_available': {'key': 'nameAvailable', 'type': 'bool'}, - 'name': {'key': 'name', 'type': 'str'}, 'message': {'key': 'message', 'type': 'str'}, 'reason': {'key': 'reason', 'type': 'str'}, } @@ -38,6 +35,5 @@ class CheckNameResult(Model): def __init__(self, **kwargs): super(CheckNameResult, self).__init__(**kwargs) self.name_available = kwargs.get('name_available', None) - self.name = kwargs.get('name', None) self.message = kwargs.get('message', None) self.reason = kwargs.get('reason', None) diff --git a/azext_iot/sdk/digitaltwins_arm/models/check_name_result_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result_py3.py similarity index 81% rename from azext_iot/sdk/digitaltwins_arm/models/check_name_result_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/check_name_result_py3.py index 5d38ec4f4..7c2daedbd 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/check_name_result_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result_py3.py @@ -18,26 +18,22 @@ class CheckNameResult(Model): :param name_available: Specifies a Boolean value that indicates if the name is available. :type name_available: bool - :param name: The name that was checked. - :type name: str :param message: Message indicating an unavailable name due to a conflict, or a description of the naming rules that are violated. :type message: str :param reason: Message providing the reason why the given name is invalid. Possible values include: 'Invalid', 'AlreadyExists' - :type reason: str or ~digitaltwins-arm.models.Reason + :type reason: str or ~azure.mgmt.digitaltwins.models.Reason """ _attribute_map = { 'name_available': {'key': 'nameAvailable', 'type': 'bool'}, - 'name': {'key': 'name', 'type': 'str'}, 'message': {'key': 'message', 'type': 'str'}, 'reason': {'key': 'reason', 'type': 'str'}, } - def __init__(self, *, name_available: bool=None, name: str=None, message: str=None, reason=None, **kwargs) -> None: + def __init__(self, *, name_available: bool=None, message: str=None, reason=None, **kwargs) -> None: super(CheckNameResult, self).__init__(**kwargs) self.name_available = name_available - self.name = name self.message = message self.reason = reason diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description.py similarity index 92% rename from azext_iot/sdk/digitaltwins_arm/models/digital_twins_description.py rename to azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description.py index 85c4fe32d..273c7e339 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description.py @@ -32,20 +32,20 @@ class DigitalTwinsDescription(DigitalTwinsResource): :type tags: dict[str, str] :ivar created_time: Time when DigitalTwinsInstance was created. :vartype created_time: datetime - :ivar last_updated_time: Time when DigitalTwinsInstance was created. + :ivar last_updated_time: Time when DigitalTwinsInstance was updated. :vartype last_updated_time: datetime :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving' :vartype provisioning_state: str or - ~digitaltwins-arm.models.ProvisioningState + ~azure.mgmt.digitaltwins.models.ProvisioningState :ivar host_name: Api endpoint to work with DigitalTwinsInstance. :vartype host_name: str """ - # @digimaun - removed SKU references _validation = { 'id': {'readonly': True}, - 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'name': {'readonly': True, 'pattern': r'^(?!-)[A-Za-z0-9-]{3,63}(?` object + A paging container for iterating over a list of :class:`DigitalTwinsDescription ` object """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_py3.py similarity index 93% rename from azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_py3.py index 14309288e..648525f75 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_description_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_py3.py @@ -32,20 +32,20 @@ class DigitalTwinsDescription(DigitalTwinsResource): :type tags: dict[str, str] :ivar created_time: Time when DigitalTwinsInstance was created. :vartype created_time: datetime - :ivar last_updated_time: Time when DigitalTwinsInstance was created. + :ivar last_updated_time: Time when DigitalTwinsInstance was updated. :vartype last_updated_time: datetime :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving' :vartype provisioning_state: str or - ~digitaltwins-arm.models.ProvisioningState + ~azure.mgmt.digitaltwins.models.ProvisioningState :ivar host_name: Api endpoint to work with DigitalTwinsInstance. :vartype host_name: str """ - # @digimaun - removed SKU references _validation = { 'id': {'readonly': True}, - 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'name': {'readonly': True, 'pattern': r'^(?!-)[A-Za-z0-9-]{3,63}(?` object + A paging container for iterating over a list of :class:`DigitalTwinsEndpointResource ` object """ - # @digimaun - current_page: [DigitalTwinsEndpointResource] -> {object] _attribute_map = { 'next_link': {'key': 'nextLink', 'type': 'str'}, - 'current_page': {'key': 'value', 'type': '[object]'} + 'current_page': {'key': 'value', 'type': '[DigitalTwinsEndpointResource]'} } def __init__(self, *args, **kwargs): diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties.py similarity index 83% rename from azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties.py rename to azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties.py index 7fc9a4f71..1db0bbf80 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties.py @@ -24,14 +24,16 @@ class DigitalTwinsEndpointResourceProperties(Model): All required parameters must be populated in order to send to Azure. :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState + ~azure.mgmt.digitaltwins.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] + :param dead_letter_secret: Dead letter storage secret. Will be obfuscated + during read. + :type dead_letter_secret: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str """ @@ -45,7 +47,7 @@ class DigitalTwinsEndpointResourceProperties(Model): _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'tags', 'type': '{str}'}, + 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, } @@ -57,5 +59,5 @@ def __init__(self, **kwargs): super(DigitalTwinsEndpointResourceProperties, self).__init__(**kwargs) self.provisioning_state = None self.created_time = None - self.tags = kwargs.get('tags', None) + self.dead_letter_secret = kwargs.get('dead_letter_secret', None) self.endpoint_type = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties_py3.py similarity index 81% rename from azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties_py3.py index 532622053..ef101f571 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_properties_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties_py3.py @@ -24,14 +24,16 @@ class DigitalTwinsEndpointResourceProperties(Model): All required parameters must be populated in order to send to Azure. :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState + ~azure.mgmt.digitaltwins.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] + :param dead_letter_secret: Dead letter storage secret. Will be obfuscated + during read. + :type dead_letter_secret: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str """ @@ -45,7 +47,7 @@ class DigitalTwinsEndpointResourceProperties(Model): _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'tags', 'type': '{str}'}, + 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, } @@ -53,9 +55,9 @@ class DigitalTwinsEndpointResourceProperties(Model): 'endpoint_type': {'ServiceBus': 'ServiceBus', 'EventHub': 'EventHub', 'EventGrid': 'EventGrid'} } - def __init__(self, *, tags=None, **kwargs) -> None: + def __init__(self, *, dead_letter_secret: str=None, **kwargs) -> None: super(DigitalTwinsEndpointResourceProperties, self).__init__(**kwargs) self.provisioning_state = None self.created_time = None - self.tags = tags + self.dead_letter_secret = dead_letter_secret self.endpoint_type = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_py3.py similarity index 64% rename from azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_py3.py index 18605393a..79def4503 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_update_info_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_py3.py @@ -12,39 +12,40 @@ from .external_resource_py3 import ExternalResource -class IntegrationResourceUpdateInfo(ExternalResource): - """IoTHub integration resource. +class DigitalTwinsEndpointResource(ExternalResource): + """DigitalTwinsInstance endpoint resource. Variables are only populated by the server, and will be ignored when sending a request. + All required parameters must be populated in order to send to Azure. + :ivar id: The resource identifier. :vartype id: str :ivar name: Extension resource name. :vartype name: str :ivar type: The resource type. :vartype type: str - :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. - Possible values include: 'Provisioning', 'Deleting', 'Succeeded', - 'Failed', 'Canceled' - :vartype provisioning_state: str or - ~digitaltwins-arm.models.IntegrationResourceState + :param properties: Required. DigitalTwinsInstance endpoint resource + properties. + :type properties: + ~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResourceProperties """ _validation = { 'id': {'readonly': True}, 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, 'type': {'readonly': True}, - 'provisioning_state': {'readonly': True}, + 'properties': {'required': True}, } _attribute_map = { 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, - 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'properties': {'key': 'properties', 'type': 'DigitalTwinsEndpointResourceProperties'}, } - def __init__(self, **kwargs) -> None: - super(IntegrationResourceUpdateInfo, self).__init__(**kwargs) - self.provisioning_state = None + def __init__(self, *, properties, **kwargs) -> None: + super(DigitalTwinsEndpointResource, self).__init__(**kwargs) + self.properties = properties diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description.py similarity index 100% rename from azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description.py rename to azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description.py diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description_py3.py similarity index 100% rename from azext_iot/sdk/digitaltwins_arm/models/digital_twins_patch_description_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description_py3.py diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource.py similarity index 92% rename from azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource.py rename to azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource.py index 3242e55c4..f5b632031 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_resource.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource.py @@ -32,10 +32,9 @@ class DigitalTwinsResource(Model): :type tags: dict[str, str] """ - # @digimaun - removed SKU references _validation = { 'id': {'readonly': True}, - 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'name': {'readonly': True, 'pattern': r'^(?!-)[A-Za-z0-9-]{3,63}(? None: diff --git a/azext_iot/sdk/digitaltwins_arm/models/error_definition.py b/azext_iot/sdk/digitaltwins/controlplane/models/error_definition.py similarity index 94% rename from azext_iot/sdk/digitaltwins_arm/models/error_definition.py rename to azext_iot/sdk/digitaltwins/controlplane/models/error_definition.py index acfd7dc85..c96f086b7 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/error_definition.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/error_definition.py @@ -24,7 +24,7 @@ class ErrorDefinition(Model): :ivar message: Description of the error. :vartype message: str :ivar details: Internal error details. - :vartype details: list[~digitaltwins-arm.models.ErrorDefinition] + :vartype details: list[~azure.mgmt.digitaltwins.models.ErrorDefinition] """ _validation = { diff --git a/azext_iot/sdk/digitaltwins_arm/models/error_definition_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/error_definition_py3.py similarity index 94% rename from azext_iot/sdk/digitaltwins_arm/models/error_definition_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/error_definition_py3.py index ca558eab6..8bb60ac87 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/error_definition_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/error_definition_py3.py @@ -24,7 +24,7 @@ class ErrorDefinition(Model): :ivar message: Description of the error. :vartype message: str :ivar details: Internal error details. - :vartype details: list[~digitaltwins-arm.models.ErrorDefinition] + :vartype details: list[~azure.mgmt.digitaltwins.models.ErrorDefinition] """ _validation = { diff --git a/azext_iot/sdk/digitaltwins_arm/models/error_response.py b/azext_iot/sdk/digitaltwins/controlplane/models/error_response.py similarity index 95% rename from azext_iot/sdk/digitaltwins_arm/models/error_response.py rename to azext_iot/sdk/digitaltwins/controlplane/models/error_response.py index ffa933ab7..ea865540f 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/error_response.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/error_response.py @@ -17,7 +17,7 @@ class ErrorResponse(Model): """Error response. :param error: Error description - :type error: ~digitaltwins-arm.models.ErrorDefinition + :type error: ~azure.mgmt.digitaltwins.models.ErrorDefinition """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins_arm/models/error_response_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/error_response_py3.py similarity index 95% rename from azext_iot/sdk/digitaltwins_arm/models/error_response_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/error_response_py3.py index 9a6d91f8c..fef3c3115 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/error_response_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/error_response_py3.py @@ -17,7 +17,7 @@ class ErrorResponse(Model): """Error response. :param error: Error description - :type error: ~digitaltwins-arm.models.ErrorDefinition + :type error: ~azure.mgmt.digitaltwins.models.ErrorDefinition """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins_arm/models/event_grid.py b/azext_iot/sdk/digitaltwins/controlplane/models/event_grid.py similarity index 78% rename from azext_iot/sdk/digitaltwins_arm/models/event_grid.py rename to azext_iot/sdk/digitaltwins/controlplane/models/event_grid.py index 6fb8b38b8..37bfbf98a 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/event_grid.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/event_grid.py @@ -13,7 +13,7 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): - """properties related to eventgrid. + """Properties related to EventGrid. Variables are only populated by the server, and will be ignored when sending a request. @@ -21,23 +21,25 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): All required parameters must be populated in order to send to Azure. :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState + ~azure.mgmt.digitaltwins.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] + :param dead_letter_secret: Dead letter storage secret. Will be obfuscated + during read. + :type dead_letter_secret: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str - :param topic_endpoint: EventGrid Topic Endpoint + :param topic_endpoint: Required. EventGrid Topic Endpoint :type topic_endpoint: str :param access_key1: Required. EventGrid secondary accesskey. Will be - obfuscated during read + obfuscated during read. :type access_key1: str - :param access_key2: Required. EventGrid secondary accesskey. Will be - obfuscated during read + :param access_key2: EventGrid secondary accesskey. Will be obfuscated + during read. :type access_key2: str """ @@ -45,14 +47,14 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): 'provisioning_state': {'readonly': True}, 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, + 'topic_endpoint': {'required': True}, 'access_key1': {'required': True}, - 'access_key2': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'tags', 'type': '{str}'}, + 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'topic_endpoint': {'key': 'TopicEndpoint', 'type': 'str'}, 'access_key1': {'key': 'accessKey1', 'type': 'str'}, diff --git a/azext_iot/sdk/digitaltwins_arm/models/event_grid_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/event_grid_py3.py similarity index 72% rename from azext_iot/sdk/digitaltwins_arm/models/event_grid_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/event_grid_py3.py index 3b445c11f..22674c4b3 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/event_grid_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/event_grid_py3.py @@ -13,7 +13,7 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): - """properties related to eventgrid. + """Properties related to EventGrid. Variables are only populated by the server, and will be ignored when sending a request. @@ -21,23 +21,25 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): All required parameters must be populated in order to send to Azure. :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState + ~azure.mgmt.digitaltwins.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] + :param dead_letter_secret: Dead letter storage secret. Will be obfuscated + during read. + :type dead_letter_secret: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str - :param topic_endpoint: EventGrid Topic Endpoint + :param topic_endpoint: Required. EventGrid Topic Endpoint :type topic_endpoint: str :param access_key1: Required. EventGrid secondary accesskey. Will be - obfuscated during read + obfuscated during read. :type access_key1: str - :param access_key2: Required. EventGrid secondary accesskey. Will be - obfuscated during read + :param access_key2: EventGrid secondary accesskey. Will be obfuscated + during read. :type access_key2: str """ @@ -45,22 +47,22 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): 'provisioning_state': {'readonly': True}, 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, + 'topic_endpoint': {'required': True}, 'access_key1': {'required': True}, - 'access_key2': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'tags', 'type': '{str}'}, + 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'topic_endpoint': {'key': 'TopicEndpoint', 'type': 'str'}, 'access_key1': {'key': 'accessKey1', 'type': 'str'}, 'access_key2': {'key': 'accessKey2', 'type': 'str'}, } - def __init__(self, *, access_key1: str, access_key2: str, tags=None, topic_endpoint: str=None, **kwargs) -> None: - super(EventGrid, self).__init__(tags=tags, **kwargs) + def __init__(self, *, topic_endpoint: str, access_key1: str, dead_letter_secret: str=None, access_key2: str=None, **kwargs) -> None: + super(EventGrid, self).__init__(dead_letter_secret=dead_letter_secret, **kwargs) self.topic_endpoint = topic_endpoint self.access_key1 = access_key1 self.access_key2 = access_key2 diff --git a/azext_iot/sdk/digitaltwins_arm/models/event_hub.py b/azext_iot/sdk/digitaltwins/controlplane/models/event_hub.py similarity index 78% rename from azext_iot/sdk/digitaltwins_arm/models/event_hub.py rename to azext_iot/sdk/digitaltwins/controlplane/models/event_hub.py index a4a06edf6..a3c70304c 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/event_hub.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/event_hub.py @@ -13,7 +13,7 @@ class EventHub(DigitalTwinsEndpointResourceProperties): - """properties related to eventhub. + """Properties related to EventHub. Variables are only populated by the server, and will be ignored when sending a request. @@ -21,21 +21,23 @@ class EventHub(DigitalTwinsEndpointResourceProperties): All required parameters must be populated in order to send to Azure. :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState + ~azure.mgmt.digitaltwins.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] + :param dead_letter_secret: Dead letter storage secret. Will be obfuscated + during read. + :type dead_letter_secret: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str :param connection_string_primary_key: Required. PrimaryConnectionString of - the endpoint. Will be obfuscated during read + the endpoint. Will be obfuscated during read. :type connection_string_primary_key: str - :param connection_string_secondary_key: Required. - SecondaryConnectionString of the endpoint. Will be obfuscated during read + :param connection_string_secondary_key: SecondaryConnectionString of the + endpoint. Will be obfuscated during read. :type connection_string_secondary_key: str """ @@ -44,16 +46,15 @@ class EventHub(DigitalTwinsEndpointResourceProperties): 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, 'connection_string_primary_key': {'required': True}, - 'connection_string_secondary_key': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'tags', 'type': '{str}'}, + 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, - 'connection_string_primary_key': {'key': 'connectionString-PrimaryKey', 'type': 'str'}, - 'connection_string_secondary_key': {'key': 'connectionString-SecondaryKey', 'type': 'str'}, + 'connection_string_primary_key': {'key': 'connectionStringPrimaryKey', 'type': 'str'}, + 'connection_string_secondary_key': {'key': 'connectionStringSecondaryKey', 'type': 'str'}, } def __init__(self, **kwargs): diff --git a/azext_iot/sdk/digitaltwins_arm/models/event_hub_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/event_hub_py3.py similarity index 71% rename from azext_iot/sdk/digitaltwins_arm/models/event_hub_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/event_hub_py3.py index 204923f9a..d76d794ec 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/event_hub_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/event_hub_py3.py @@ -13,7 +13,7 @@ class EventHub(DigitalTwinsEndpointResourceProperties): - """properties related to eventhub. + """Properties related to EventHub. Variables are only populated by the server, and will be ignored when sending a request. @@ -21,21 +21,23 @@ class EventHub(DigitalTwinsEndpointResourceProperties): All required parameters must be populated in order to send to Azure. :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState + ~azure.mgmt.digitaltwins.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] + :param dead_letter_secret: Dead letter storage secret. Will be obfuscated + during read. + :type dead_letter_secret: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str :param connection_string_primary_key: Required. PrimaryConnectionString of - the endpoint. Will be obfuscated during read + the endpoint. Will be obfuscated during read. :type connection_string_primary_key: str - :param connection_string_secondary_key: Required. - SecondaryConnectionString of the endpoint. Will be obfuscated during read + :param connection_string_secondary_key: SecondaryConnectionString of the + endpoint. Will be obfuscated during read. :type connection_string_secondary_key: str """ @@ -44,20 +46,19 @@ class EventHub(DigitalTwinsEndpointResourceProperties): 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, 'connection_string_primary_key': {'required': True}, - 'connection_string_secondary_key': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'tags', 'type': '{str}'}, + 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, - 'connection_string_primary_key': {'key': 'connectionString-PrimaryKey', 'type': 'str'}, - 'connection_string_secondary_key': {'key': 'connectionString-SecondaryKey', 'type': 'str'}, + 'connection_string_primary_key': {'key': 'connectionStringPrimaryKey', 'type': 'str'}, + 'connection_string_secondary_key': {'key': 'connectionStringSecondaryKey', 'type': 'str'}, } - def __init__(self, *, connection_string_primary_key: str, connection_string_secondary_key: str, tags=None, **kwargs) -> None: - super(EventHub, self).__init__(tags=tags, **kwargs) + def __init__(self, *, connection_string_primary_key: str, dead_letter_secret: str=None, connection_string_secondary_key: str=None, **kwargs) -> None: + super(EventHub, self).__init__(dead_letter_secret=dead_letter_secret, **kwargs) self.connection_string_primary_key = connection_string_primary_key self.connection_string_secondary_key = connection_string_secondary_key self.endpoint_type = 'EventHub' diff --git a/azext_iot/sdk/digitaltwins_arm/models/external_resource.py b/azext_iot/sdk/digitaltwins/controlplane/models/external_resource.py similarity index 97% rename from azext_iot/sdk/digitaltwins_arm/models/external_resource.py rename to azext_iot/sdk/digitaltwins/controlplane/models/external_resource.py index 695691d7a..a22ca0a06 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/external_resource.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/external_resource.py @@ -13,7 +13,7 @@ class ExternalResource(Model): - """Definition of a Resource. + """Definition of a resource. Variables are only populated by the server, and will be ignored when sending a request. diff --git a/azext_iot/sdk/digitaltwins_arm/models/external_resource_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/external_resource_py3.py similarity index 97% rename from azext_iot/sdk/digitaltwins_arm/models/external_resource_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/external_resource_py3.py index aefe1ccdb..83ed4d616 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/external_resource_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/external_resource_py3.py @@ -13,7 +13,7 @@ class ExternalResource(Model): - """Definition of a Resource. + """Definition of a resource. Variables are only populated by the server, and will be ignored when sending a request. diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation.py b/azext_iot/sdk/digitaltwins/controlplane/models/operation.py similarity index 69% rename from azext_iot/sdk/digitaltwins_arm/models/operation.py rename to azext_iot/sdk/digitaltwins/controlplane/models/operation.py index a26283e41..71aafcc50 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/operation.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/operation.py @@ -22,19 +22,30 @@ class Operation(Model): delete} :vartype name: str :param display: Operation properties display - :type display: ~digitaltwins-arm.models.OperationDisplay + :type display: ~azure.mgmt.digitaltwins.models.OperationDisplay + :ivar origin: The intended executor of the operation. + :vartype origin: str + :ivar is_data_action: If the operation is a data action (for data plane + rbac). + :vartype is_data_action: bool """ _validation = { 'name': {'readonly': True}, + 'origin': {'readonly': True}, + 'is_data_action': {'readonly': True}, } _attribute_map = { 'name': {'key': 'name', 'type': 'str'}, 'display': {'key': 'display', 'type': 'OperationDisplay'}, + 'origin': {'key': 'origin', 'type': 'str'}, + 'is_data_action': {'key': 'isDataAction', 'type': 'bool'}, } def __init__(self, **kwargs): super(Operation, self).__init__(**kwargs) self.name = None self.display = kwargs.get('display', None) + self.origin = None + self.is_data_action = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation_display.py b/azext_iot/sdk/digitaltwins/controlplane/models/operation_display.py similarity index 100% rename from azext_iot/sdk/digitaltwins_arm/models/operation_display.py rename to azext_iot/sdk/digitaltwins/controlplane/models/operation_display.py diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation_display_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/operation_display_py3.py similarity index 100% rename from azext_iot/sdk/digitaltwins_arm/models/operation_display_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/operation_display_py3.py diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation_paged.py b/azext_iot/sdk/digitaltwins/controlplane/models/operation_paged.py similarity index 93% rename from azext_iot/sdk/digitaltwins_arm/models/operation_paged.py rename to azext_iot/sdk/digitaltwins/controlplane/models/operation_paged.py index 6ce7d4c1c..ae64e3a7d 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/operation_paged.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/operation_paged.py @@ -14,7 +14,7 @@ class OperationPaged(Paged): """ - A paging container for iterating over a list of :class:`Operation ` object + A paging container for iterating over a list of :class:`Operation ` object """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins_arm/models/operation_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/operation_py3.py similarity index 69% rename from azext_iot/sdk/digitaltwins_arm/models/operation_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/operation_py3.py index 24014c3b3..f87e6da83 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/operation_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/operation_py3.py @@ -22,19 +22,30 @@ class Operation(Model): delete} :vartype name: str :param display: Operation properties display - :type display: ~digitaltwins-arm.models.OperationDisplay + :type display: ~azure.mgmt.digitaltwins.models.OperationDisplay + :ivar origin: The intended executor of the operation. + :vartype origin: str + :ivar is_data_action: If the operation is a data action (for data plane + rbac). + :vartype is_data_action: bool """ _validation = { 'name': {'readonly': True}, + 'origin': {'readonly': True}, + 'is_data_action': {'readonly': True}, } _attribute_map = { 'name': {'key': 'name', 'type': 'str'}, 'display': {'key': 'display', 'type': 'OperationDisplay'}, + 'origin': {'key': 'origin', 'type': 'str'}, + 'is_data_action': {'key': 'isDataAction', 'type': 'bool'}, } def __init__(self, *, display=None, **kwargs) -> None: super(Operation, self).__init__(**kwargs) self.name = None self.display = display + self.origin = None + self.is_data_action = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/service_bus.py b/azext_iot/sdk/digitaltwins/controlplane/models/service_bus.py similarity index 80% rename from azext_iot/sdk/digitaltwins_arm/models/service_bus.py rename to azext_iot/sdk/digitaltwins/controlplane/models/service_bus.py index 133c04e22..033ec34e8 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/service_bus.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/service_bus.py @@ -13,7 +13,7 @@ class ServiceBus(DigitalTwinsEndpointResourceProperties): - """properties related to servicebus. + """Properties related to ServiceBus. Variables are only populated by the server, and will be ignored when sending a request. @@ -21,21 +21,23 @@ class ServiceBus(DigitalTwinsEndpointResourceProperties): All required parameters must be populated in order to send to Azure. :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState + ~azure.mgmt.digitaltwins.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] + :param dead_letter_secret: Dead letter storage secret. Will be obfuscated + during read. + :type dead_letter_secret: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str :param primary_connection_string: Required. PrimaryConnectionString of the - endpoint. Will be obfuscated during read + endpoint. Will be obfuscated during read. :type primary_connection_string: str - :param secondary_connection_string: Required. SecondaryConnectionString of - the endpoint. Will be obfuscated during read + :param secondary_connection_string: SecondaryConnectionString of the + endpoint. Will be obfuscated during read. :type secondary_connection_string: str """ @@ -44,13 +46,12 @@ class ServiceBus(DigitalTwinsEndpointResourceProperties): 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, 'primary_connection_string': {'required': True}, - 'secondary_connection_string': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'tags', 'type': '{str}'}, + 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'primary_connection_string': {'key': 'primaryConnectionString', 'type': 'str'}, 'secondary_connection_string': {'key': 'secondaryConnectionString', 'type': 'str'}, diff --git a/azext_iot/sdk/digitaltwins_arm/models/service_bus_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/service_bus_py3.py similarity index 73% rename from azext_iot/sdk/digitaltwins_arm/models/service_bus_py3.py rename to azext_iot/sdk/digitaltwins/controlplane/models/service_bus_py3.py index 84a2a7ab6..80ad6758f 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/service_bus_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/service_bus_py3.py @@ -13,7 +13,7 @@ class ServiceBus(DigitalTwinsEndpointResourceProperties): - """properties related to servicebus. + """Properties related to ServiceBus. Variables are only populated by the server, and will be ignored when sending a request. @@ -21,21 +21,23 @@ class ServiceBus(DigitalTwinsEndpointResourceProperties): All required parameters must be populated in order to send to Azure. :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' + 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', + 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState + ~azure.mgmt.digitaltwins.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] + :param dead_letter_secret: Dead letter storage secret. Will be obfuscated + during read. + :type dead_letter_secret: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str :param primary_connection_string: Required. PrimaryConnectionString of the - endpoint. Will be obfuscated during read + endpoint. Will be obfuscated during read. :type primary_connection_string: str - :param secondary_connection_string: Required. SecondaryConnectionString of - the endpoint. Will be obfuscated during read + :param secondary_connection_string: SecondaryConnectionString of the + endpoint. Will be obfuscated during read. :type secondary_connection_string: str """ @@ -44,20 +46,19 @@ class ServiceBus(DigitalTwinsEndpointResourceProperties): 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, 'primary_connection_string': {'required': True}, - 'secondary_connection_string': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'tags', 'type': '{str}'}, + 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'primary_connection_string': {'key': 'primaryConnectionString', 'type': 'str'}, 'secondary_connection_string': {'key': 'secondaryConnectionString', 'type': 'str'}, } - def __init__(self, *, primary_connection_string: str, secondary_connection_string: str, tags=None, **kwargs) -> None: - super(ServiceBus, self).__init__(tags=tags, **kwargs) + def __init__(self, *, primary_connection_string: str, dead_letter_secret: str=None, secondary_connection_string: str=None, **kwargs) -> None: + super(ServiceBus, self).__init__(dead_letter_secret=dead_letter_secret, **kwargs) self.primary_connection_string = primary_connection_string self.secondary_connection_string = secondary_connection_string self.endpoint_type = 'ServiceBus' diff --git a/azext_iot/sdk/digitaltwins_arm/operations/__init__.py b/azext_iot/sdk/digitaltwins/controlplane/operations/__init__.py similarity index 100% rename from azext_iot/sdk/digitaltwins_arm/operations/__init__.py rename to azext_iot/sdk/digitaltwins/controlplane/operations/__init__.py diff --git a/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_endpoint_operations.py b/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_endpoint_operations.py similarity index 85% rename from azext_iot/sdk/digitaltwins_arm/operations/digital_twins_endpoint_operations.py rename to azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_endpoint_operations.py index e569ac027..e5b62cefb 100644 --- a/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_endpoint_operations.py +++ b/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_endpoint_operations.py @@ -24,7 +24,7 @@ class DigitalTwinsEndpointOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-03-01-preview". + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-10-31". """ models = models @@ -34,7 +34,7 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-03-01-preview" + self.api_version = "2020-10-31" self.config = config @@ -54,9 +54,9 @@ def list( overrides`. :return: An iterator like instance of DigitalTwinsEndpointResource :rtype: - ~digitaltwins-arm.models.DigitalTwinsEndpointResourcePaged[~digitaltwins-arm.models.DigitalTwinsEndpointResource] + ~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResourcePaged[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ def internal_paging(next_link=None, raw=False): @@ -66,7 +66,7 @@ def internal_paging(next_link=None, raw=False): path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), - 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(?`. :return: DigitalTwinsEndpointResource or ClientRawResponse if raw=true - :rtype: ~digitaltwins-arm.models.DigitalTwinsEndpointResource or - ~msrest.pipeline.ClientRawResponse + :rtype: ~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource + or ~msrest.pipeline.ClientRawResponse :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ # Construct URL url = self.get.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), - 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1), - 'endpointName': self._serialize.url("endpoint_name", endpoint_name, 'str', max_length=64, min_length=1, pattern=r'^[A-Za-z0-9-._]{1,64}$') + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? {object} if response.status_code == 200: - deserialized = self._deserialize('{object}', response) + deserialized = self._deserialize('DigitalTwinsEndpointResource', response) if raw: client_raw_response = ClientRawResponse(deserialized, response) @@ -174,18 +173,18 @@ def get( return deserialized get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}/endpoints/{endpointName}'} - # @digimaun - custom add properties + def _create_or_update_initial( self, resource_group_name, resource_name, endpoint_name, properties, custom_headers=None, raw=False, **operation_config): - endpoint_description = properties + endpoint_description = models.DigitalTwinsEndpointResource(properties=properties) # Construct URL url = self.create_or_update.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), - 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1), - 'endpointName': self._serialize.url("endpoint_name", endpoint_name, 'str', max_length=64, min_length=1, pattern=r'^[A-Za-z0-9-._]{1,64}$') + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True :rtype: - ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsEndpointResource] + ~msrestazure.azure_operation.AzureOperationPoller[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource] or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsEndpointResource]] + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource]] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ raw_result = self._create_or_update_initial( resource_group_name=resource_group_name, @@ -264,9 +265,8 @@ def create_or_update( **operation_config ) - # @digimaun - DigitalTwinsEndpointResource -> {object} def get_long_running_output(response): - deserialized = self._deserialize('{object}', response) + deserialized = self._deserialize('DigitalTwinsEndpointResource', response) if raw: client_raw_response = ClientRawResponse(deserialized, response) @@ -291,8 +291,8 @@ def _delete_initial( path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), - 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1), - 'endpointName': self._serialize.url("endpoint_name", endpoint_name, 'str', max_length=64, min_length=1, pattern=r'^[A-Za-z0-9-._]{1,64}$') + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True :rtype: - ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsEndpointResource] + ~msrestazure.azure_operation.AzureOperationPoller[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource] or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsEndpointResource]] + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource]] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ raw_result = self._delete_initial( resource_group_name=resource_group_name, diff --git a/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py b/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_operations.py similarity index 87% rename from azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py rename to azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_operations.py index a9a040cde..3a9daa7d6 100644 --- a/azext_iot/sdk/digitaltwins_arm/operations/digital_twins_operations.py +++ b/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_operations.py @@ -24,7 +24,7 @@ class DigitalTwinsOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-03-01-preview". + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-10-31". """ models = models @@ -34,7 +34,7 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-03-01-preview" + self.api_version = "2020-10-31" self.config = config @@ -53,17 +53,17 @@ def get( :param operation_config: :ref:`Operation configuration overrides`. :return: DigitalTwinsDescription or ClientRawResponse if raw=true - :rtype: ~digitaltwins-arm.models.DigitalTwinsDescription or + :rtype: ~azure.mgmt.digitaltwins.models.DigitalTwinsDescription or ~msrest.pipeline.ClientRawResponse :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ # Construct URL url = self.get.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), - 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True :rtype: - ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsDescription] + ~msrestazure.azure_operation.AzureOperationPoller[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription] or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsDescription]] + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription]] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ raw_result = self._create_or_update_initial( resource_group_name=resource_group_name, @@ -209,9 +210,28 @@ def get_long_running_output(response): return LROPoller(self._client, raw_result, get_long_running_output, polling_method) create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}'} - - def _update_initial( + def update( self, resource_group_name, resource_name, tags=None, custom_headers=None, raw=False, **operation_config): + """Update metadata of DigitalTwinsInstance. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param tags: Instance tags + :type tags: dict[str, str] + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: DigitalTwinsDescription or ClientRawResponse if raw=true + :rtype: ~azure.mgmt.digitaltwins.models.DigitalTwinsDescription or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ digital_twins_patch_description = models.DigitalTwinsPatchDescription(tags=tags) # Construct URL @@ -219,7 +239,7 @@ def _update_initial( path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), - 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True - :rtype: - ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsDescription] - or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsDescription]] - :raises: - :class:`ErrorResponseException` - """ - raw_result = self._update_initial( - resource_group_name=resource_group_name, - resource_name=resource_name, - tags=tags, - custom_headers=custom_headers, - raw=True, - **operation_config - ) - - def get_long_running_output(response): - deserialized = self._deserialize('DigitalTwinsDescription', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - - lro_delay = operation_config.get( - 'long_running_operation_timeout', - self.config.long_running_operation_timeout) - if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) - elif polling is False: polling_method = NoPolling() - else: polling_method = polling - return LROPoller(self._client, raw_result, get_long_running_output, polling_method) update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}'} @@ -319,7 +288,7 @@ def _delete_initial( path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), - 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=64, min_length=1) + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? {object} - deserialized = self._deserialize('{object}', response) + deserialized = self._deserialize('DigitalTwinsDescription', response) if raw: client_raw_response = ClientRawResponse(deserialized, response) @@ -373,11 +343,11 @@ def delete( :return: An instance of LROPoller that returns DigitalTwinsDescription or ClientRawResponse if raw==True :rtype: - ~msrestazure.azure_operation.AzureOperationPoller[~digitaltwins-arm.models.DigitalTwinsDescription] + ~msrestazure.azure_operation.AzureOperationPoller[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription] or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~digitaltwins-arm.models.DigitalTwinsDescription]] + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription]] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ raw_result = self._delete_initial( resource_group_name=resource_group_name, @@ -387,9 +357,8 @@ def delete( **operation_config ) - # @digimaun - DigitalTwinsDescription -> {object} def get_long_running_output(response): - deserialized = self._deserialize('{object}', response) + deserialized = self._deserialize('DigitalTwinsDescription', response) if raw: client_raw_response = ClientRawResponse(deserialized, response) @@ -417,11 +386,12 @@ def list( overrides`. :return: An iterator like instance of DigitalTwinsDescription :rtype: - ~digitaltwins-arm.models.DigitalTwinsDescriptionPaged[~digitaltwins-arm.models.DigitalTwinsDescription] + ~azure.mgmt.digitaltwins.models.DigitalTwinsDescriptionPaged[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ def internal_paging(next_link=None, raw=False): + if not next_link: # Construct URL url = self.list.metadata['url'] @@ -482,9 +452,9 @@ def list_by_resource_group( overrides`. :return: An iterator like instance of DigitalTwinsDescription :rtype: - ~digitaltwins-arm.models.DigitalTwinsDescriptionPaged[~digitaltwins-arm.models.DigitalTwinsDescription] + ~azure.mgmt.digitaltwins.models.DigitalTwinsDescriptionPaged[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ def internal_paging(next_link=None, raw=False): @@ -549,10 +519,10 @@ def check_name_availability( :param operation_config: :ref:`Operation configuration overrides`. :return: CheckNameResult or ClientRawResponse if raw=true - :rtype: ~digitaltwins-arm.models.CheckNameResult or + :rtype: ~azure.mgmt.digitaltwins.models.CheckNameResult or ~msrest.pipeline.ClientRawResponse :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ digital_twins_instance_check_name = models.CheckNameRequest(name=name) diff --git a/azext_iot/sdk/digitaltwins_arm/operations/operations.py b/azext_iot/sdk/digitaltwins/controlplane/operations/operations.py similarity index 92% rename from azext_iot/sdk/digitaltwins_arm/operations/operations.py rename to azext_iot/sdk/digitaltwins/controlplane/operations/operations.py index dc525f21c..bf1060b5d 100644 --- a/azext_iot/sdk/digitaltwins_arm/operations/operations.py +++ b/azext_iot/sdk/digitaltwins/controlplane/operations/operations.py @@ -22,7 +22,7 @@ class Operations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-03-01-preview". + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-10-31". """ models = models @@ -32,7 +32,7 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-03-01-preview" + self.api_version = "2020-10-31" self.config = config @@ -47,9 +47,9 @@ def list( overrides`. :return: An iterator like instance of Operation :rtype: - ~digitaltwins-arm.models.OperationPaged[~digitaltwins-arm.models.Operation] + ~azure.mgmt.digitaltwins.models.OperationPaged[~azure.mgmt.digitaltwins.models.Operation] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ def internal_paging(next_link=None, raw=False): diff --git a/azext_iot/sdk/digitaltwins/version.py b/azext_iot/sdk/digitaltwins/controlplane/version.py similarity index 93% rename from azext_iot/sdk/digitaltwins/version.py rename to azext_iot/sdk/digitaltwins/controlplane/version.py index aa93dc9e0..ac2482d2d 100644 --- a/azext_iot/sdk/digitaltwins/version.py +++ b/azext_iot/sdk/digitaltwins/controlplane/version.py @@ -9,5 +9,5 @@ # regenerated. # -------------------------------------------------------------------------- -VERSION = "2020-05-31-preview" +VERSION = "2020-10-31" diff --git a/azext_iot/sdk/digitaltwins/dataplane/__init__.py b/azext_iot/sdk/digitaltwins/dataplane/__init__.py new file mode 100644 index 000000000..bd6770159 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .azure_digital_twins_api import AzureDigitalTwinsAPI +from .version import VERSION + +__all__ = ['AzureDigitalTwinsAPI'] + +__version__ = VERSION + diff --git a/azext_iot/sdk/digitaltwins/azure_digital_twins_api.py b/azext_iot/sdk/digitaltwins/dataplane/azure_digital_twins_api.py similarity index 98% rename from azext_iot/sdk/digitaltwins/azure_digital_twins_api.py rename to azext_iot/sdk/digitaltwins/dataplane/azure_digital_twins_api.py index 77d3012e6..531cd0cc3 100644 --- a/azext_iot/sdk/digitaltwins/azure_digital_twins_api.py +++ b/azext_iot/sdk/digitaltwins/dataplane/azure_digital_twins_api.py @@ -76,7 +76,7 @@ def __init__( super(AzureDigitalTwinsAPI, self).__init__(self.config.credentials, self.config) client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2020-05-31-preview' + self.api_version = '2020-10-31' self._serialize = Serializer(client_models) self._deserialize = Deserializer(client_models) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/__init__.py b/azext_iot/sdk/digitaltwins/dataplane/models/__init__.py new file mode 100644 index 000000000..bf7a6fc63 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/__init__.py @@ -0,0 +1,120 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +try: + from .event_route_py3 import EventRoute + from .digital_twins_model_data_py3 import DigitalTwinsModelData + from .incoming_relationship_py3 import IncomingRelationship + from .query_specification_py3 import QuerySpecification + from .query_result_py3 import QueryResult + from .inner_error_py3 import InnerError + from .error_py3 import Error + from .error_response_py3 import ErrorResponse, ErrorResponseException + from .digital_twin_models_add_options_py3 import DigitalTwinModelsAddOptions + from .digital_twin_models_list_options_py3 import DigitalTwinModelsListOptions + from .digital_twin_models_get_by_id_options_py3 import DigitalTwinModelsGetByIdOptions + from .digital_twin_models_update_options_py3 import DigitalTwinModelsUpdateOptions + from .digital_twin_models_delete_options_py3 import DigitalTwinModelsDeleteOptions + from .query_query_twins_options_py3 import QueryQueryTwinsOptions + from .digital_twins_get_by_id_options_py3 import DigitalTwinsGetByIdOptions + from .digital_twins_add_options_py3 import DigitalTwinsAddOptions + from .digital_twins_delete_options_py3 import DigitalTwinsDeleteOptions + from .digital_twins_update_options_py3 import DigitalTwinsUpdateOptions + from .digital_twins_get_relationship_by_id_options_py3 import DigitalTwinsGetRelationshipByIdOptions + from .digital_twins_add_relationship_options_py3 import DigitalTwinsAddRelationshipOptions + from .digital_twins_delete_relationship_options_py3 import DigitalTwinsDeleteRelationshipOptions + from .digital_twins_update_relationship_options_py3 import DigitalTwinsUpdateRelationshipOptions + from .digital_twins_list_relationships_options_py3 import DigitalTwinsListRelationshipsOptions + from .digital_twins_list_incoming_relationships_options_py3 import DigitalTwinsListIncomingRelationshipsOptions + from .digital_twins_send_telemetry_options_py3 import DigitalTwinsSendTelemetryOptions + from .digital_twins_send_component_telemetry_options_py3 import DigitalTwinsSendComponentTelemetryOptions + from .digital_twins_get_component_options_py3 import DigitalTwinsGetComponentOptions + from .digital_twins_update_component_options_py3 import DigitalTwinsUpdateComponentOptions + from .event_routes_list_options_py3 import EventRoutesListOptions + from .event_routes_get_by_id_options_py3 import EventRoutesGetByIdOptions + from .event_routes_add_options_py3 import EventRoutesAddOptions + from .event_routes_delete_options_py3 import EventRoutesDeleteOptions +except (SyntaxError, ImportError): + from .event_route import EventRoute + from .digital_twins_model_data import DigitalTwinsModelData + from .incoming_relationship import IncomingRelationship + from .query_specification import QuerySpecification + from .query_result import QueryResult + from .inner_error import InnerError + from .error import Error + from .error_response import ErrorResponse, ErrorResponseException + from .digital_twin_models_add_options import DigitalTwinModelsAddOptions + from .digital_twin_models_list_options import DigitalTwinModelsListOptions + from .digital_twin_models_get_by_id_options import DigitalTwinModelsGetByIdOptions + from .digital_twin_models_update_options import DigitalTwinModelsUpdateOptions + from .digital_twin_models_delete_options import DigitalTwinModelsDeleteOptions + from .query_query_twins_options import QueryQueryTwinsOptions + from .digital_twins_get_by_id_options import DigitalTwinsGetByIdOptions + from .digital_twins_add_options import DigitalTwinsAddOptions + from .digital_twins_delete_options import DigitalTwinsDeleteOptions + from .digital_twins_update_options import DigitalTwinsUpdateOptions + from .digital_twins_get_relationship_by_id_options import DigitalTwinsGetRelationshipByIdOptions + from .digital_twins_add_relationship_options import DigitalTwinsAddRelationshipOptions + from .digital_twins_delete_relationship_options import DigitalTwinsDeleteRelationshipOptions + from .digital_twins_update_relationship_options import DigitalTwinsUpdateRelationshipOptions + from .digital_twins_list_relationships_options import DigitalTwinsListRelationshipsOptions + from .digital_twins_list_incoming_relationships_options import DigitalTwinsListIncomingRelationshipsOptions + from .digital_twins_send_telemetry_options import DigitalTwinsSendTelemetryOptions + from .digital_twins_send_component_telemetry_options import DigitalTwinsSendComponentTelemetryOptions + from .digital_twins_get_component_options import DigitalTwinsGetComponentOptions + from .digital_twins_update_component_options import DigitalTwinsUpdateComponentOptions + from .event_routes_list_options import EventRoutesListOptions + from .event_routes_get_by_id_options import EventRoutesGetByIdOptions + from .event_routes_add_options import EventRoutesAddOptions + from .event_routes_delete_options import EventRoutesDeleteOptions +from .digital_twins_model_data_paged import DigitalTwinsModelDataPaged +from .object_paged import ObjectPaged +from .incoming_relationship_paged import IncomingRelationshipPaged +from .event_route_paged import EventRoutePaged + +__all__ = [ + 'EventRoute', + 'DigitalTwinsModelData', + 'IncomingRelationship', + 'QuerySpecification', + 'QueryResult', + 'InnerError', + 'Error', + 'ErrorResponse', 'ErrorResponseException', + 'DigitalTwinModelsAddOptions', + 'DigitalTwinModelsListOptions', + 'DigitalTwinModelsGetByIdOptions', + 'DigitalTwinModelsUpdateOptions', + 'DigitalTwinModelsDeleteOptions', + 'QueryQueryTwinsOptions', + 'DigitalTwinsGetByIdOptions', + 'DigitalTwinsAddOptions', + 'DigitalTwinsDeleteOptions', + 'DigitalTwinsUpdateOptions', + 'DigitalTwinsGetRelationshipByIdOptions', + 'DigitalTwinsAddRelationshipOptions', + 'DigitalTwinsDeleteRelationshipOptions', + 'DigitalTwinsUpdateRelationshipOptions', + 'DigitalTwinsListRelationshipsOptions', + 'DigitalTwinsListIncomingRelationshipsOptions', + 'DigitalTwinsSendTelemetryOptions', + 'DigitalTwinsSendComponentTelemetryOptions', + 'DigitalTwinsGetComponentOptions', + 'DigitalTwinsUpdateComponentOptions', + 'EventRoutesListOptions', + 'EventRoutesGetByIdOptions', + 'EventRoutesAddOptions', + 'EventRoutesDeleteOptions', + 'DigitalTwinsModelDataPaged', + 'ObjectPaged', + 'IncomingRelationshipPaged', + 'EventRoutePaged', +] diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_add_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_add_options.py new file mode 100644 index 000000000..48badddbc --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_add_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsAddOptions(Model): + """Additional parameters for add operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinModelsAddOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_add_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_add_options_py3.py new file mode 100644 index 000000000..400b238d3 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_add_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsAddOptions(Model): + """Additional parameters for add operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinModelsAddOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_delete_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_delete_options.py new file mode 100644 index 000000000..45ea0a951 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_delete_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsDeleteOptions(Model): + """Additional parameters for delete operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinModelsDeleteOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_delete_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_delete_options_py3.py new file mode 100644 index 000000000..006373249 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_delete_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsDeleteOptions(Model): + """Additional parameters for delete operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinModelsDeleteOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_get_by_id_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_get_by_id_options.py new file mode 100644 index 000000000..8f7ec8e26 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_get_by_id_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsGetByIdOptions(Model): + """Additional parameters for get_by_id operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinModelsGetByIdOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_get_by_id_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_get_by_id_options_py3.py new file mode 100644 index 000000000..c0c2412ee --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_get_by_id_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsGetByIdOptions(Model): + """Additional parameters for get_by_id operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinModelsGetByIdOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_list_options.py similarity index 52% rename from azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options.py rename to azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_list_options.py index dff617960..3fad49f0d 100644 --- a/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options.py +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_list_options.py @@ -15,16 +15,25 @@ class DigitalTwinModelsListOptions(Model): """Additional parameters for list operation. - :param max_item_count: The maximum number of items to retrieve per - request. The server may choose to return less than the requested max. - Default value: -1 . - :type max_item_count: int + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param max_items_per_page: The maximum number of items to retrieve per + request. The server may choose to return less than the requested number. + :type max_items_per_page: int """ _attribute_map = { - 'max_item_count': {'key': '', 'type': 'int'}, + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'max_items_per_page': {'key': '', 'type': 'int'}, } def __init__(self, **kwargs): super(DigitalTwinModelsListOptions, self).__init__(**kwargs) - self.max_item_count = kwargs.get('max_item_count', -1) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.max_items_per_page = kwargs.get('max_items_per_page', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_list_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_list_options_py3.py new file mode 100644 index 000000000..75bffa726 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_list_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsListOptions(Model): + """Additional parameters for list operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param max_items_per_page: The maximum number of items to retrieve per + request. The server may choose to return less than the requested number. + :type max_items_per_page: int + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'max_items_per_page': {'key': '', 'type': 'int'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, max_items_per_page: int=None, **kwargs) -> None: + super(DigitalTwinModelsListOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.max_items_per_page = max_items_per_page diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_update_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_update_options.py new file mode 100644 index 000000000..25015b17e --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_update_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsUpdateOptions(Model): + """Additional parameters for update operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinModelsUpdateOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_update_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_update_options_py3.py new file mode 100644 index 000000000..a47f2d9ff --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twin_models_update_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinModelsUpdateOptions(Model): + """Additional parameters for update operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinModelsUpdateOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_options.py new file mode 100644 index 000000000..3cdd70078 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_options.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsAddOptions(Model): + """Additional parameters for add operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_none_match: Only perform the operation if the entity does not + already exist. Possible values include: '*' + :type if_none_match: str or ~digitaltwins.models.enum + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_none_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsAddOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.if_none_match = kwargs.get('if_none_match', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_options_py3.py new file mode 100644 index 000000000..288bd24e1 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsAddOptions(Model): + """Additional parameters for add operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_none_match: Only perform the operation if the entity does not + already exist. Possible values include: '*' + :type if_none_match: str or ~digitaltwins.models.enum + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_none_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, if_none_match=None, **kwargs) -> None: + super(DigitalTwinsAddOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.if_none_match = if_none_match diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_relationship_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_relationship_options.py new file mode 100644 index 000000000..2be7fd3c4 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_relationship_options.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsAddRelationshipOptions(Model): + """Additional parameters for add_relationship operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_none_match: Only perform the operation if the entity does not + already exist. Possible values include: '*' + :type if_none_match: str or ~digitaltwins.models.enum + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_none_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsAddRelationshipOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.if_none_match = kwargs.get('if_none_match', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_relationship_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_relationship_options_py3.py new file mode 100644 index 000000000..b5ade7953 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_add_relationship_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsAddRelationshipOptions(Model): + """Additional parameters for add_relationship operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_none_match: Only perform the operation if the entity does not + already exist. Possible values include: '*' + :type if_none_match: str or ~digitaltwins.models.enum + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_none_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, if_none_match=None, **kwargs) -> None: + super(DigitalTwinsAddRelationshipOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.if_none_match = if_none_match diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_options.py new file mode 100644 index 000000000..9286f6091 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_options.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsDeleteOptions(Model): + """Additional parameters for delete operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsDeleteOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.if_match = kwargs.get('if_match', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_options_py3.py new file mode 100644 index 000000000..70548af15 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsDeleteOptions(Model): + """Additional parameters for delete operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, if_match: str=None, **kwargs) -> None: + super(DigitalTwinsDeleteOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.if_match = if_match diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_relationship_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_relationship_options.py new file mode 100644 index 000000000..049be2bf3 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_relationship_options.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsDeleteRelationshipOptions(Model): + """Additional parameters for delete_relationship operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsDeleteRelationshipOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.if_match = kwargs.get('if_match', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_relationship_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_relationship_options_py3.py new file mode 100644 index 000000000..bb9aa5e53 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_delete_relationship_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsDeleteRelationshipOptions(Model): + """Additional parameters for delete_relationship operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, if_match: str=None, **kwargs) -> None: + super(DigitalTwinsDeleteRelationshipOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.if_match = if_match diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_by_id_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_by_id_options.py new file mode 100644 index 000000000..7a769771b --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_by_id_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsGetByIdOptions(Model): + """Additional parameters for get_by_id operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsGetByIdOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_by_id_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_by_id_options_py3.py new file mode 100644 index 000000000..43042e223 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_by_id_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsGetByIdOptions(Model): + """Additional parameters for get_by_id operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinsGetByIdOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_component_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_component_options.py new file mode 100644 index 000000000..a2228fe74 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_component_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsGetComponentOptions(Model): + """Additional parameters for get_component operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsGetComponentOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_component_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_component_options_py3.py new file mode 100644 index 000000000..a12a5074c --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_component_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsGetComponentOptions(Model): + """Additional parameters for get_component operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinsGetComponentOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_relationship_by_id_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_relationship_by_id_options.py new file mode 100644 index 000000000..6e670a7da --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_relationship_by_id_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsGetRelationshipByIdOptions(Model): + """Additional parameters for get_relationship_by_id operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsGetRelationshipByIdOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_relationship_by_id_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_relationship_by_id_options_py3.py new file mode 100644 index 000000000..6607d075e --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_get_relationship_by_id_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsGetRelationshipByIdOptions(Model): + """Additional parameters for get_relationship_by_id operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinsGetRelationshipByIdOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_incoming_relationships_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_incoming_relationships_options.py new file mode 100644 index 000000000..8a64942cd --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_incoming_relationships_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsListIncomingRelationshipsOptions(Model): + """Additional parameters for list_incoming_relationships operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsListIncomingRelationshipsOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_incoming_relationships_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_incoming_relationships_options_py3.py new file mode 100644 index 000000000..726b262f9 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_incoming_relationships_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsListIncomingRelationshipsOptions(Model): + """Additional parameters for list_incoming_relationships operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinsListIncomingRelationshipsOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_relationships_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_relationships_options.py new file mode 100644 index 000000000..6e3847224 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_relationships_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsListRelationshipsOptions(Model): + """Additional parameters for list_relationships operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsListRelationshipsOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_relationships_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_relationships_options_py3.py new file mode 100644 index 000000000..2247449d3 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_list_relationships_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsListRelationshipsOptions(Model): + """Additional parameters for list_relationships operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinsListRelationshipsOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/models/model_data.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_model_data.py similarity index 87% rename from azext_iot/sdk/digitaltwins/models/model_data.py rename to azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_model_data.py index db3fa699c..ecfd9c8b5 100644 --- a/azext_iot/sdk/digitaltwins/models/model_data.py +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_model_data.py @@ -12,17 +12,17 @@ from msrest.serialization import Model -class ModelData(Model): +class DigitalTwinsModelData(Model): """A model definition and metadata for that model. All required parameters must be populated in order to send to Azure. :param display_name: A language map that contains the localized display names as specified in the model definition. - :type display_name: object + :type display_name: dict[str, str] :param description: A language map that contains the localized descriptions as specified in the model definition. - :type description: object + :type description: dict[str, str] :param id: Required. The id of the model as specified in the model definition. :type id: str @@ -41,8 +41,8 @@ class ModelData(Model): } _attribute_map = { - 'display_name': {'key': 'displayName', 'type': 'object'}, - 'description': {'key': 'description', 'type': 'object'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'description': {'key': 'description', 'type': '{str}'}, 'id': {'key': 'id', 'type': 'str'}, 'upload_time': {'key': 'uploadTime', 'type': 'iso-8601'}, 'decommissioned': {'key': 'decommissioned', 'type': 'bool'}, @@ -50,7 +50,7 @@ class ModelData(Model): } def __init__(self, **kwargs): - super(ModelData, self).__init__(**kwargs) + super(DigitalTwinsModelData, self).__init__(**kwargs) self.display_name = kwargs.get('display_name', None) self.description = kwargs.get('description', None) self.id = kwargs.get('id', None) diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_paged.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_model_data_paged.py similarity index 67% rename from azext_iot/sdk/digitaltwins_arm/models/integration_resource_paged.py rename to azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_model_data_paged.py index 4fc4e48b4..661260b1f 100644 --- a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_paged.py +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_model_data_paged.py @@ -12,16 +12,16 @@ from msrest.paging import Paged -class IntegrationResourcePaged(Paged): +class DigitalTwinsModelDataPaged(Paged): """ - A paging container for iterating over a list of :class:`IntegrationResource ` object + A paging container for iterating over a list of :class:`DigitalTwinsModelData ` object """ _attribute_map = { 'next_link': {'key': 'nextLink', 'type': 'str'}, - 'current_page': {'key': 'value', 'type': '[IntegrationResource]'} + 'current_page': {'key': 'value', 'type': '[DigitalTwinsModelData]'} } def __init__(self, *args, **kwargs): - super(IntegrationResourcePaged, self).__init__(*args, **kwargs) + super(DigitalTwinsModelDataPaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins/models/model_data_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_model_data_py3.py similarity index 87% rename from azext_iot/sdk/digitaltwins/models/model_data_py3.py rename to azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_model_data_py3.py index 667e2fb2e..70505731d 100644 --- a/azext_iot/sdk/digitaltwins/models/model_data_py3.py +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_model_data_py3.py @@ -12,17 +12,17 @@ from msrest.serialization import Model -class ModelData(Model): +class DigitalTwinsModelData(Model): """A model definition and metadata for that model. All required parameters must be populated in order to send to Azure. :param display_name: A language map that contains the localized display names as specified in the model definition. - :type display_name: object + :type display_name: dict[str, str] :param description: A language map that contains the localized descriptions as specified in the model definition. - :type description: object + :type description: dict[str, str] :param id: Required. The id of the model as specified in the model definition. :type id: str @@ -41,8 +41,8 @@ class ModelData(Model): } _attribute_map = { - 'display_name': {'key': 'displayName', 'type': 'object'}, - 'description': {'key': 'description', 'type': 'object'}, + 'display_name': {'key': 'displayName', 'type': '{str}'}, + 'description': {'key': 'description', 'type': '{str}'}, 'id': {'key': 'id', 'type': 'str'}, 'upload_time': {'key': 'uploadTime', 'type': 'iso-8601'}, 'decommissioned': {'key': 'decommissioned', 'type': 'bool'}, @@ -50,7 +50,7 @@ class ModelData(Model): } def __init__(self, *, id: str, display_name=None, description=None, upload_time=None, decommissioned: bool=False, model=None, **kwargs) -> None: - super(ModelData, self).__init__(**kwargs) + super(DigitalTwinsModelData, self).__init__(**kwargs) self.display_name = display_name self.description = description self.id = id diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_component_telemetry_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_component_telemetry_options.py new file mode 100644 index 000000000..22b9d2e74 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_component_telemetry_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsSendComponentTelemetryOptions(Model): + """Additional parameters for send_component_telemetry operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsSendComponentTelemetryOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_component_telemetry_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_component_telemetry_options_py3.py new file mode 100644 index 000000000..278917a31 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_component_telemetry_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsSendComponentTelemetryOptions(Model): + """Additional parameters for send_component_telemetry operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinsSendComponentTelemetryOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_telemetry_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_telemetry_options.py new file mode 100644 index 000000000..30a51aa17 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_telemetry_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsSendTelemetryOptions(Model): + """Additional parameters for send_telemetry operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsSendTelemetryOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_telemetry_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_telemetry_options_py3.py new file mode 100644 index 000000000..895781371 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_send_telemetry_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsSendTelemetryOptions(Model): + """Additional parameters for send_telemetry operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(DigitalTwinsSendTelemetryOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_component_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_component_options.py new file mode 100644 index 000000000..f2395dbf0 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_component_options.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsUpdateComponentOptions(Model): + """Additional parameters for update_component operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsUpdateComponentOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.if_match = kwargs.get('if_match', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_component_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_component_options_py3.py new file mode 100644 index 000000000..334416799 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_component_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsUpdateComponentOptions(Model): + """Additional parameters for update_component operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, if_match: str=None, **kwargs) -> None: + super(DigitalTwinsUpdateComponentOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.if_match = if_match diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_options.py new file mode 100644 index 000000000..9a9cb33e9 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_options.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsUpdateOptions(Model): + """Additional parameters for update operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsUpdateOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.if_match = kwargs.get('if_match', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_options_py3.py new file mode 100644 index 000000000..7dcd4a2d7 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsUpdateOptions(Model): + """Additional parameters for update operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, if_match: str=None, **kwargs) -> None: + super(DigitalTwinsUpdateOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.if_match = if_match diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_relationship_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_relationship_options.py new file mode 100644 index 000000000..2fc4e0eb1 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_relationship_options.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsUpdateRelationshipOptions(Model): + """Additional parameters for update_relationship operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsUpdateRelationshipOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.if_match = kwargs.get('if_match', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_relationship_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_relationship_options_py3.py new file mode 100644 index 000000000..0c5f880d0 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/digital_twins_update_relationship_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsUpdateRelationshipOptions(Model): + """Additional parameters for update_relationship operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param if_match: Only perform the operation if the entity's etag matches + one of the etags provided or * is provided. + :type if_match: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'if_match': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, if_match: str=None, **kwargs) -> None: + super(DigitalTwinsUpdateRelationshipOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.if_match = if_match diff --git a/azext_iot/sdk/digitaltwins/models/error.py b/azext_iot/sdk/digitaltwins/dataplane/models/error.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/error.py rename to azext_iot/sdk/digitaltwins/dataplane/models/error.py diff --git a/azext_iot/sdk/digitaltwins/models/error_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/error_py3.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/error_py3.py rename to azext_iot/sdk/digitaltwins/dataplane/models/error_py3.py diff --git a/azext_iot/sdk/digitaltwins/models/error_response.py b/azext_iot/sdk/digitaltwins/dataplane/models/error_response.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/error_response.py rename to azext_iot/sdk/digitaltwins/dataplane/models/error_response.py diff --git a/azext_iot/sdk/digitaltwins/models/error_response_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/error_response_py3.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/error_response_py3.py rename to azext_iot/sdk/digitaltwins/dataplane/models/error_response_py3.py diff --git a/azext_iot/sdk/digitaltwins/models/event_route.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_route.py similarity index 83% rename from azext_iot/sdk/digitaltwins/models/event_route.py rename to azext_iot/sdk/digitaltwins/dataplane/models/event_route.py index f98c05c2c..945fe652a 100644 --- a/azext_iot/sdk/digitaltwins/models/event_route.py +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_route.py @@ -24,18 +24,18 @@ class EventRoute(Model): :ivar id: The id of the event route. :vartype id: str - :param endpoint_id: Required. The id of the endpoint this event route is - bound to. - :type endpoint_id: str - :param filter: An expression which describes the events which are routed - to the endpoint. + :param endpoint_name: Required. The name of the endpoint this event route + is bound to. + :type endpoint_name: str + :param filter: Required. An expression which describes the events which + are routed to the endpoint. :type filter: str """ - # @digimaun - endpointId/endpoint_id is now endpointName/endpoint_name _validation = { 'id': {'readonly': True}, 'endpoint_name': {'required': True}, + 'filter': {'required': True}, } _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins/models/event_route_paged.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_route_paged.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/event_route_paged.py rename to azext_iot/sdk/digitaltwins/dataplane/models/event_route_paged.py diff --git a/azext_iot/sdk/digitaltwins/models/event_route_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_route_py3.py similarity index 79% rename from azext_iot/sdk/digitaltwins/models/event_route_py3.py rename to azext_iot/sdk/digitaltwins/dataplane/models/event_route_py3.py index b3e3a58c6..c7508a44b 100644 --- a/azext_iot/sdk/digitaltwins/models/event_route_py3.py +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_route_py3.py @@ -24,18 +24,18 @@ class EventRoute(Model): :ivar id: The id of the event route. :vartype id: str - :param endpoint_id: Required. The id of the endpoint this event route is - bound to. - :type endpoint_id: str - :param filter: An expression which describes the events which are routed - to the endpoint. + :param endpoint_name: Required. The name of the endpoint this event route + is bound to. + :type endpoint_name: str + :param filter: Required. An expression which describes the events which + are routed to the endpoint. :type filter: str """ - # @digimaun - endpointId/endpoint_id is now endpointName/endpoint_name _validation = { 'id': {'readonly': True}, 'endpoint_name': {'required': True}, + 'filter': {'required': True}, } _attribute_map = { @@ -44,7 +44,7 @@ class EventRoute(Model): 'filter': {'key': 'filter', 'type': 'str'}, } - def __init__(self, *, endpoint_name: str, filter: str=None, **kwargs) -> None: + def __init__(self, *, endpoint_name: str, filter: str, **kwargs) -> None: super(EventRoute, self).__init__(**kwargs) self.id = None self.endpoint_name = endpoint_name diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_add_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_add_options.py new file mode 100644 index 000000000..59227319e --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_add_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoutesAddOptions(Model): + """Additional parameters for add operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(EventRoutesAddOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_add_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_add_options_py3.py new file mode 100644 index 000000000..0a296fab6 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_add_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoutesAddOptions(Model): + """Additional parameters for add operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(EventRoutesAddOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_delete_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_delete_options.py new file mode 100644 index 000000000..c572c6e60 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_delete_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoutesDeleteOptions(Model): + """Additional parameters for delete operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(EventRoutesDeleteOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_delete_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_delete_options_py3.py new file mode 100644 index 000000000..b333cfbcf --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_delete_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoutesDeleteOptions(Model): + """Additional parameters for delete operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(EventRoutesDeleteOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_get_by_id_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_get_by_id_options.py new file mode 100644 index 000000000..a36ab7d0c --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_get_by_id_options.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoutesGetByIdOptions(Model): + """Additional parameters for get_by_id operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(EventRoutesGetByIdOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_get_by_id_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_get_by_id_options_py3.py new file mode 100644 index 000000000..c8bb4c3f1 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_get_by_id_options_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoutesGetByIdOptions(Model): + """Additional parameters for get_by_id operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, **kwargs) -> None: + super(EventRoutesGetByIdOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate diff --git a/azext_iot/sdk/digitaltwins/models/event_routes_list_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_list_options.py similarity index 52% rename from azext_iot/sdk/digitaltwins/models/event_routes_list_options.py rename to azext_iot/sdk/digitaltwins/dataplane/models/event_routes_list_options.py index ae16d6034..f66b58b96 100644 --- a/azext_iot/sdk/digitaltwins/models/event_routes_list_options.py +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_list_options.py @@ -15,16 +15,25 @@ class EventRoutesListOptions(Model): """Additional parameters for list operation. - :param max_item_count: The maximum number of items to retrieve per - request. The server may choose to return less than the requested max. - Default value: -1 . - :type max_item_count: int + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param max_items_per_page: The maximum number of items to retrieve per + request. The server may choose to return less than the requested number. + :type max_items_per_page: int """ _attribute_map = { - 'max_item_count': {'key': '', 'type': 'int'}, + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'max_items_per_page': {'key': '', 'type': 'int'}, } def __init__(self, **kwargs): super(EventRoutesListOptions, self).__init__(**kwargs) - self.max_item_count = kwargs.get('max_item_count', -1) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.max_items_per_page = kwargs.get('max_items_per_page', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_list_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_list_options_py3.py new file mode 100644 index 000000000..f523dabe0 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/event_routes_list_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class EventRoutesListOptions(Model): + """Additional parameters for list operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param max_items_per_page: The maximum number of items to retrieve per + request. The server may choose to return less than the requested number. + :type max_items_per_page: int + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'max_items_per_page': {'key': '', 'type': 'int'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, max_items_per_page: int=None, **kwargs) -> None: + super(EventRoutesListOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.max_items_per_page = max_items_per_page diff --git a/azext_iot/sdk/digitaltwins/models/incoming_relationship.py b/azext_iot/sdk/digitaltwins/dataplane/models/incoming_relationship.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/incoming_relationship.py rename to azext_iot/sdk/digitaltwins/dataplane/models/incoming_relationship.py diff --git a/azext_iot/sdk/digitaltwins/models/incoming_relationship_paged.py b/azext_iot/sdk/digitaltwins/dataplane/models/incoming_relationship_paged.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/incoming_relationship_paged.py rename to azext_iot/sdk/digitaltwins/dataplane/models/incoming_relationship_paged.py diff --git a/azext_iot/sdk/digitaltwins/models/incoming_relationship_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/incoming_relationship_py3.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/incoming_relationship_py3.py rename to azext_iot/sdk/digitaltwins/dataplane/models/incoming_relationship_py3.py diff --git a/azext_iot/sdk/digitaltwins/models/inner_error.py b/azext_iot/sdk/digitaltwins/dataplane/models/inner_error.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/inner_error.py rename to azext_iot/sdk/digitaltwins/dataplane/models/inner_error.py diff --git a/azext_iot/sdk/digitaltwins/models/inner_error_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/inner_error_py3.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/inner_error_py3.py rename to azext_iot/sdk/digitaltwins/dataplane/models/inner_error_py3.py diff --git a/azext_iot/sdk/digitaltwins/models/object_paged.py b/azext_iot/sdk/digitaltwins/dataplane/models/object_paged.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/object_paged.py rename to azext_iot/sdk/digitaltwins/dataplane/models/object_paged.py diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/query_query_twins_options.py b/azext_iot/sdk/digitaltwins/dataplane/models/query_query_twins_options.py new file mode 100644 index 000000000..45739d288 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/query_query_twins_options.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class QueryQueryTwinsOptions(Model): + """Additional parameters for query_twins operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param max_items_per_page: The maximum number of items to retrieve per + request. The server may choose to return less than the requested number. + :type max_items_per_page: int + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'max_items_per_page': {'key': '', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(QueryQueryTwinsOptions, self).__init__(**kwargs) + self.traceparent = kwargs.get('traceparent', None) + self.tracestate = kwargs.get('tracestate', None) + self.max_items_per_page = kwargs.get('max_items_per_page', None) diff --git a/azext_iot/sdk/digitaltwins/dataplane/models/query_query_twins_options_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/query_query_twins_options_py3.py new file mode 100644 index 000000000..81a3dcb12 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/dataplane/models/query_query_twins_options_py3.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class QueryQueryTwinsOptions(Model): + """Additional parameters for query_twins operation. + + :param traceparent: Identifies the request in a distributed tracing + system. + :type traceparent: str + :param tracestate: Provides vendor-specific trace identification + information and is a companion to traceparent. + :type tracestate: str + :param max_items_per_page: The maximum number of items to retrieve per + request. The server may choose to return less than the requested number. + :type max_items_per_page: int + """ + + _attribute_map = { + 'traceparent': {'key': '', 'type': 'str'}, + 'tracestate': {'key': '', 'type': 'str'}, + 'max_items_per_page': {'key': '', 'type': 'int'}, + } + + def __init__(self, *, traceparent: str=None, tracestate: str=None, max_items_per_page: int=None, **kwargs) -> None: + super(QueryQueryTwinsOptions, self).__init__(**kwargs) + self.traceparent = traceparent + self.tracestate = tracestate + self.max_items_per_page = max_items_per_page diff --git a/azext_iot/sdk/digitaltwins/models/query_result.py b/azext_iot/sdk/digitaltwins/dataplane/models/query_result.py similarity index 86% rename from azext_iot/sdk/digitaltwins/models/query_result.py rename to azext_iot/sdk/digitaltwins/dataplane/models/query_result.py index 7349e2290..2af8ae62d 100644 --- a/azext_iot/sdk/digitaltwins/models/query_result.py +++ b/azext_iot/sdk/digitaltwins/dataplane/models/query_result.py @@ -15,19 +15,19 @@ class QueryResult(Model): """The results of a query operation and an optional continuation token. - :param items: The query results. - :type items: list[object] + :param value: The query results. + :type value: list[object] :param continuation_token: A token which can be used to construct a new QuerySpecification to retrieve the next set of results. :type continuation_token: str """ _attribute_map = { - 'items': {'key': 'items', 'type': '[object]'}, + 'value': {'key': 'value', 'type': '[object]'}, 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, } def __init__(self, **kwargs): super(QueryResult, self).__init__(**kwargs) - self.items = kwargs.get('items', None) + self.value = kwargs.get('value', None) self.continuation_token = kwargs.get('continuation_token', None) diff --git a/azext_iot/sdk/digitaltwins/models/query_result_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/query_result_py3.py similarity index 82% rename from azext_iot/sdk/digitaltwins/models/query_result_py3.py rename to azext_iot/sdk/digitaltwins/dataplane/models/query_result_py3.py index ef38eb6d8..72c68e909 100644 --- a/azext_iot/sdk/digitaltwins/models/query_result_py3.py +++ b/azext_iot/sdk/digitaltwins/dataplane/models/query_result_py3.py @@ -15,19 +15,19 @@ class QueryResult(Model): """The results of a query operation and an optional continuation token. - :param items: The query results. - :type items: list[object] + :param value: The query results. + :type value: list[object] :param continuation_token: A token which can be used to construct a new QuerySpecification to retrieve the next set of results. :type continuation_token: str """ _attribute_map = { - 'items': {'key': 'items', 'type': '[object]'}, + 'value': {'key': 'value', 'type': '[object]'}, 'continuation_token': {'key': 'continuationToken', 'type': 'str'}, } - def __init__(self, *, items=None, continuation_token: str=None, **kwargs) -> None: + def __init__(self, *, value=None, continuation_token: str=None, **kwargs) -> None: super(QueryResult, self).__init__(**kwargs) - self.items = items + self.value = value self.continuation_token = continuation_token diff --git a/azext_iot/sdk/digitaltwins/models/query_specification.py b/azext_iot/sdk/digitaltwins/dataplane/models/query_specification.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/query_specification.py rename to azext_iot/sdk/digitaltwins/dataplane/models/query_specification.py diff --git a/azext_iot/sdk/digitaltwins/models/query_specification_py3.py b/azext_iot/sdk/digitaltwins/dataplane/models/query_specification_py3.py similarity index 100% rename from azext_iot/sdk/digitaltwins/models/query_specification_py3.py rename to azext_iot/sdk/digitaltwins/dataplane/models/query_specification_py3.py diff --git a/azext_iot/sdk/digitaltwins/operations/__init__.py b/azext_iot/sdk/digitaltwins/dataplane/operations/__init__.py similarity index 100% rename from azext_iot/sdk/digitaltwins/operations/__init__.py rename to azext_iot/sdk/digitaltwins/dataplane/operations/__init__.py diff --git a/azext_iot/sdk/digitaltwins/operations/digital_twin_models_operations.py b/azext_iot/sdk/digitaltwins/dataplane/operations/digital_twin_models_operations.py similarity index 65% rename from azext_iot/sdk/digitaltwins/operations/digital_twin_models_operations.py rename to azext_iot/sdk/digitaltwins/dataplane/operations/digital_twin_models_operations.py index 7c39155cc..32851bfbe 100644 --- a/azext_iot/sdk/digitaltwins/operations/digital_twin_models_operations.py +++ b/azext_iot/sdk/digitaltwins/dataplane/operations/digital_twin_models_operations.py @@ -22,7 +22,7 @@ class DigitalTwinModelsOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: The requested API version. Constant value: "2020-05-31-preview". + :ivar api_version: The API version to use for the request. Constant value: "2020-10-31". """ models = models @@ -32,32 +32,49 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-05-31-preview" + self.api_version = "2020-10-31" self.config = config def add( - self, models=None, custom_headers=None, raw=False, **operation_config): + self, models=None, digital_twin_models_add_options=None, custom_headers=None, raw=False, **operation_config): """Uploads one or more models. When any error occurs, no models are uploaded. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 409 (Conflict): One or more of the provided models already exist. + * 201 Created + * 400 Bad Request + * DTDLParserError - The models provided are not valid DTDL. + * InvalidArgument - The model id is invalid. + * LimitExceeded - The maximum number of model ids allowed in + 'dependenciesFor' has been reached. + * ModelVersionNotSupported - The version of DTDL used is not supported. + * 409 Conflict + * ModelAlreadyExists - The model provided already exists. :param models: An array of models to add. :type models: list[object] + :param digital_twin_models_add_options: Additional parameters for the + operation + :type digital_twin_models_add_options: + ~digitaltwins.models.DigitalTwinModelsAddOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. :return: list or ClientRawResponse if raw=true - :rtype: list[~digitaltwins.models.ModelData] or + :rtype: list[~digitaltwins.models.DigitalTwinsModelData] or ~msrest.pipeline.ClientRawResponse :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twin_models_add_options is not None: + traceparent = digital_twin_models_add_options.traceparent + tracestate = None + if digital_twin_models_add_options is not None: + tracestate = digital_twin_models_add_options.tracestate + # Construct URL url = self.add.metadata['url'] @@ -75,6 +92,10 @@ def add( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct body if models is not None: @@ -88,14 +109,20 @@ def add( # @digimaun - custom response handling return response + add.metadata = {'url': '/models'} def list( self, dependencies_for=None, include_model_definition=False, digital_twin_models_list_options=None, custom_headers=None, raw=False, **operation_config): """Retrieves model metadata and, optionally, model definitions. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. + * 200 OK + * 400 Bad Request + * InvalidArgument - The model id is invalid. + * LimitExceeded - The maximum number of model ids allowed in + 'dependenciesFor' has been reached. + * 404 Not Found + * ModelNotFound - The model was not found. :param dependencies_for: The set of the models which will have their dependencies retrieved. If omitted, all models are retrieved. @@ -112,15 +139,21 @@ def list( deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: An iterator like instance of ModelData + :return: An iterator like instance of DigitalTwinsModelData :rtype: - ~digitaltwins.models.ModelDataPaged[~digitaltwins.models.ModelData] + ~digitaltwins.models.DigitalTwinsModelDataPaged[~digitaltwins.models.DigitalTwinsModelData] :raises: :class:`ErrorResponseException` """ - max_item_count = None + traceparent = None + if digital_twin_models_list_options is not None: + traceparent = digital_twin_models_list_options.traceparent + tracestate = None if digital_twin_models_list_options is not None: - max_item_count = digital_twin_models_list_options.max_item_count + tracestate = digital_twin_models_list_options.tracestate + max_items_per_page = None + if digital_twin_models_list_options is not None: + max_items_per_page = digital_twin_models_list_options.max_items_per_page def internal_paging(next_link=None, raw=False): @@ -157,8 +190,12 @@ def internal_paging(next_link=None, raw=False): header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - if max_item_count is not None: - header_parameters['x-ms-max-item-count'] = self._serialize.header("max_item_count", max_item_count, 'int') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if max_items_per_page is not None: + header_parameters['max-items-per-page'] = self._serialize.header("max_items_per_page", max_items_per_page, 'int') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -170,22 +207,26 @@ def internal_paging(next_link=None, raw=False): return response # Deserialize response - deserialized = models.ModelDataPaged(internal_paging, self._deserialize.dependencies) + deserialized = models.DigitalTwinsModelDataPaged(internal_paging, self._deserialize.dependencies) if raw: header_dict = {} - client_raw_response = models.ModelDataPaged(internal_paging, self._deserialize.dependencies, header_dict) + client_raw_response = models.DigitalTwinsModelDataPaged(internal_paging, self._deserialize.dependencies, header_dict) return client_raw_response return deserialized list.metadata = {'url': '/models'} def get_by_id( - self, id, include_model_definition=False, custom_headers=None, raw=False, **operation_config): + self, id, include_model_definition=False, digital_twin_models_get_by_id_options=None, custom_headers=None, raw=False, **operation_config): """Retrieves model metadata and optionally the model definition. Status codes: - 200 (OK): Success. - 404 (Not Found): There is no model with the provided id. + * 200 OK + * 400 Bad Request + * InvalidArgument - The model id is invalid. + * MissingArgument - The model id was not provided. + * 404 Not Found + * ModelNotFound - The model was not found. :param id: The id for the model. The id is globally unique and case sensitive. @@ -193,17 +234,28 @@ def get_by_id( :param include_model_definition: When true the model definition will be returned as part of the result. :type include_model_definition: bool + :param digital_twin_models_get_by_id_options: Additional parameters + for the operation + :type digital_twin_models_get_by_id_options: + ~digitaltwins.models.DigitalTwinModelsGetByIdOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: ModelData or ClientRawResponse if raw=true - :rtype: ~digitaltwins.models.ModelData or + :return: DigitalTwinsModelData or ClientRawResponse if raw=true + :rtype: ~digitaltwins.models.DigitalTwinsModelData or ~msrest.pipeline.ClientRawResponse :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twin_models_get_by_id_options is not None: + traceparent = digital_twin_models_get_by_id_options.traceparent + tracestate = None + if digital_twin_models_get_by_id_options is not None: + tracestate = digital_twin_models_get_by_id_options.tracestate + # Construct URL url = self.get_by_id.metadata['url'] path_format_arguments = { @@ -226,6 +278,10 @@ def get_by_id( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -237,7 +293,7 @@ def get_by_id( deserialized = None if response.status_code == 200: - deserialized = self._deserialize('ModelData', response) + deserialized = self._deserialize('DigitalTwinsModelData', response) if raw: client_raw_response = ClientRawResponse(deserialized, response) @@ -247,19 +303,30 @@ def get_by_id( get_by_id.metadata = {'url': '/models/{id}'} def update( - self, id, update_model, custom_headers=None, raw=False, **operation_config): + self, update_model, id, digital_twin_models_update_options=None, custom_headers=None, raw=False, **operation_config): """Updates the metadata for a model. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is no model with the provided id. + * 204 No Content + * 400 Bad Request + * InvalidArgument - The model id is invalid. + * JsonPatchInvalid - The JSON Patch provided is invalid. + * MissingArgument - The model id was not provided. + * 404 Not Found + * ModelNotFound - The model was not found. + * 409 Conflict + * ModelReferencesNotDecommissioned - The model refers to models that + are not decommissioned. - :param id: The id for the model. The id is globally unique and case - sensitive. - :type id: str :param update_model: An update specification described by JSON Patch. Only the decommissioned property can be replaced. :type update_model: list[object] + :param id: The id for the model. The id is globally unique and case + sensitive. + :type id: str + :param digital_twin_models_update_options: Additional parameters for + the operation + :type digital_twin_models_update_options: + ~digitaltwins.models.DigitalTwinModelsUpdateOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -270,6 +337,13 @@ def update( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twin_models_update_options is not None: + traceparent = digital_twin_models_update_options.traceparent + tracestate = None + if digital_twin_models_update_options is not None: + tracestate = digital_twin_models_update_options.tracestate + # Construct URL url = self.update.metadata['url'] path_format_arguments = { @@ -290,6 +364,10 @@ def update( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct body body_content = self._serialize.body(update_model, '[object]') @@ -307,19 +385,27 @@ def update( update.metadata = {'url': '/models/{id}'} def delete( - self, id, custom_headers=None, raw=False, **operation_config): + self, id, digital_twin_models_delete_options=None, custom_headers=None, raw=False, **operation_config): """Deletes a model. A model can only be deleted if no other models reference it. Status codes: - 204 (No Content): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is no model with the provided id. - 409 (Conflict): There are dependencies on the model that prevent it - from being deleted. + * 204 No Content + * 400 Bad Request + * InvalidArgument - The model id is invalid. + * MissingArgument - The model id was not provided. + * 404 Not Found + * ModelNotFound - The model was not found. + * 409 Conflict + * ModelReferencesNotDeleted - The model refers to models that are not + deleted. :param id: The id for the model. The id is globally unique and case sensitive. :type id: str + :param digital_twin_models_delete_options: Additional parameters for + the operation + :type digital_twin_models_delete_options: + ~digitaltwins.models.DigitalTwinModelsDeleteOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -330,6 +416,13 @@ def delete( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twin_models_delete_options is not None: + traceparent = digital_twin_models_delete_options.traceparent + tracestate = None + if digital_twin_models_delete_options is not None: + tracestate = digital_twin_models_delete_options.tracestate + # Construct URL url = self.delete.metadata['url'] path_format_arguments = { @@ -349,6 +442,10 @@ def delete( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct and send request request = self._client.delete(url, query_parameters, header_parameters) diff --git a/azext_iot/sdk/digitaltwins/operations/digital_twins_operations.py b/azext_iot/sdk/digitaltwins/dataplane/operations/digital_twins_operations.py similarity index 64% rename from azext_iot/sdk/digitaltwins/operations/digital_twins_operations.py rename to azext_iot/sdk/digitaltwins/dataplane/operations/digital_twins_operations.py index e6a205893..1d9e1ea35 100644 --- a/azext_iot/sdk/digitaltwins/operations/digital_twins_operations.py +++ b/azext_iot/sdk/digitaltwins/dataplane/operations/digital_twins_operations.py @@ -22,7 +22,7 @@ class DigitalTwinsOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: The requested API version. Constant value: "2020-05-31-preview". + :ivar api_version: The API version to use for the request. Constant value: "2020-10-31". """ models = models @@ -32,20 +32,27 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-05-31-preview" + self.api_version = "2020-10-31" self.config = config def get_by_id( - self, id, custom_headers=None, raw=False, **operation_config): + self, id, digital_twins_get_by_id_options=None, custom_headers=None, raw=False, **operation_config): """Retrieves a digital twin. Status codes: - 200 (OK): Success. - 404 (Not Found): There is no digital twin with the provided id. + * 200 OK + * 400 Bad Request + * InvalidArgument - The digital twin id is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. :param id: The id of the digital twin. The id is unique within the service and case sensitive. :type id: str + :param digital_twins_get_by_id_options: Additional parameters for the + operation + :type digital_twins_get_by_id_options: + ~digitaltwins.models.DigitalTwinsGetByIdOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -56,6 +63,13 @@ def get_by_id( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_get_by_id_options is not None: + traceparent = digital_twins_get_by_id_options.traceparent + tracestate = None + if digital_twins_get_by_id_options is not None: + tracestate = digital_twins_get_by_id_options.tracestate + # Construct URL url = self.get_by_id.metadata['url'] path_format_arguments = { @@ -76,6 +90,10 @@ def get_by_id( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -102,23 +120,31 @@ def get_by_id( get_by_id.metadata = {'url': '/digitaltwins/{id}'} def add( - self, id, twin, if_none_match=None, custom_headers=None, raw=False, **operation_config): + self, twin, id, digital_twins_add_options=None, custom_headers=None, raw=False, **operation_config): """Adds or replaces a digital twin. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 412 (Precondition Failed): The model is decommissioned or the digital - twin already exists (when using If-None-Match: *). + * 200 OK + * 400 Bad Request + * InvalidArgument - The digital twin id or payload is invalid. + * ModelDecommissioned - The model for the digital twin is + decommissioned. + * TwinLimitReached - The maximum number of digital twins allowed has + been reached. + * ValidationFailed - The digital twin payload is not valid. + * 412 Precondition Failed + * PreconditionFailed - The precondition check (If-Match or + If-None-Match) failed. - :param id: The id of the digital twin. The id is unique within the - service and case sensitive. - :type id: str :param twin: The digital twin instance being added. If provided, the $dtId property is ignored. :type twin: object - :param if_none_match: Only perform the operation if the entity does - not already exist. Possible values include: '*' - :type if_none_match: str + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param digital_twins_add_options: Additional parameters for the + operation + :type digital_twins_add_options: + ~digitaltwins.models.DigitalTwinsAddOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -129,6 +155,16 @@ def add( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_add_options is not None: + traceparent = digital_twins_add_options.traceparent + tracestate = None + if digital_twins_add_options is not None: + tracestate = digital_twins_add_options.tracestate + if_none_match = None + if digital_twins_add_options is not None: + if_none_match = digital_twins_add_options.if_none_match + # Construct URL url = self.add.metadata['url'] path_format_arguments = { @@ -148,10 +184,14 @@ def add( header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - if if_none_match is not None: - header_parameters['If-None-Match'] = self._serialize.header("if_none_match", if_none_match, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if if_none_match is not None: + header_parameters['If-None-Match'] = self._serialize.header("if_none_match", if_none_match, 'str') # Construct body body_content = self._serialize.body(twin, 'object') @@ -181,20 +221,27 @@ def add( add.metadata = {'url': '/digitaltwins/{id}'} def delete( - self, id, if_match=None, custom_headers=None, raw=False, **operation_config): + self, id, digital_twins_delete_options=None, custom_headers=None, raw=False, **operation_config): """Deletes a digital twin. All relationships referencing the digital twin must already be deleted. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is no digital twin with the provided id. + * 204 No Content + * 400 Bad Request + * InvalidArgument - The digital twin id is invalid. + * RelationshipsNotDeleted - The digital twin contains relationships. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. + * 412 Precondition Failed + * PreconditionFailed - The precondition check (If-Match or + If-None-Match) failed. :param id: The id of the digital twin. The id is unique within the service and case sensitive. :type id: str - :param if_match: Only perform the operation if the entity's etag - matches one of the etags provided or * is provided. - :type if_match: str + :param digital_twins_delete_options: Additional parameters for the + operation + :type digital_twins_delete_options: + ~digitaltwins.models.DigitalTwinsDeleteOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -205,6 +252,16 @@ def delete( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_delete_options is not None: + traceparent = digital_twins_delete_options.traceparent + tracestate = None + if digital_twins_delete_options is not None: + tracestate = digital_twins_delete_options.tracestate + if_match = None + if digital_twins_delete_options is not None: + if_match = digital_twins_delete_options.if_match + # Construct URL url = self.delete.metadata['url'] path_format_arguments = { @@ -222,10 +279,14 @@ def delete( header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') # Construct and send request request = self._client.delete(url, query_parameters, header_parameters) @@ -240,23 +301,32 @@ def delete( delete.metadata = {'url': '/digitaltwins/{id}'} def update( - self, id, patch_document, if_match=None, custom_headers=None, raw=False, **operation_config): + self, patch_document, id, digital_twins_update_options=None, custom_headers=None, raw=False, **operation_config): """Updates a digital twin. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is no digital twin with the provided id. + * 204 No Content + * 400 Bad Request + * InvalidArgument - The digital twin id or payload is invalid. + * JsonPatchInvalid - The JSON Patch provided is invalid. + * ValidationFailed - Applying the patch results in an invalid digital + twin. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. + * 412 Precondition Failed + * PreconditionFailed - The precondition check (If-Match or + If-None-Match) failed. - :param id: The id of the digital twin. The id is unique within the - service and case sensitive. - :type id: str :param patch_document: An update specification described by JSON Patch. Updates to property values and $model elements may happen in the same request. Operations are limited to add, replace and remove. :type patch_document: list[object] - :param if_match: Only perform the operation if the entity's etag - matches one of the etags provided or * is provided. - :type if_match: str + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param digital_twins_update_options: Additional parameters for the + operation + :type digital_twins_update_options: + ~digitaltwins.models.DigitalTwinsUpdateOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -267,6 +337,16 @@ def update( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_update_options is not None: + traceparent = digital_twins_update_options.traceparent + tracestate = None + if digital_twins_update_options is not None: + tracestate = digital_twins_update_options.tracestate + if_match = None + if digital_twins_update_options is not None: + if_match = digital_twins_update_options.if_match + # Construct URL url = self.update.metadata['url'] path_format_arguments = { @@ -285,10 +365,14 @@ def update( header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') # Construct body body_content = self._serialize.body(patch_document, '[object]') @@ -309,12 +393,15 @@ def update( update.metadata = {'url': '/digitaltwins/{id}'} def get_relationship_by_id( - self, id, relationship_id, custom_headers=None, raw=False, **operation_config): + self, id, relationship_id, digital_twins_get_relationship_by_id_options=None, custom_headers=None, raw=False, **operation_config): """Retrieves a relationship between two digital twins. Status codes: - 200 (OK): Success. - 404 (Not Found): There is either no digital twin or relationship with - the provided id. + * 200 OK + * 400 Bad Request + * InvalidArgument - The digital twin id or relationship id is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. + * RelationshipNotFound - The relationship was not found. :param id: The id of the digital twin. The id is unique within the service and case sensitive. @@ -322,6 +409,10 @@ def get_relationship_by_id( :param relationship_id: The id of the relationship. The id is unique within the digital twin and case sensitive. :type relationship_id: str + :param digital_twins_get_relationship_by_id_options: Additional + parameters for the operation + :type digital_twins_get_relationship_by_id_options: + ~digitaltwins.models.DigitalTwinsGetRelationshipByIdOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -332,6 +423,13 @@ def get_relationship_by_id( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_get_relationship_by_id_options is not None: + traceparent = digital_twins_get_relationship_by_id_options.traceparent + tracestate = None + if digital_twins_get_relationship_by_id_options is not None: + tracestate = digital_twins_get_relationship_by_id_options.tracestate + # Construct URL url = self.get_relationship_by_id.metadata['url'] path_format_arguments = { @@ -353,6 +451,10 @@ def get_relationship_by_id( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -379,26 +481,37 @@ def get_relationship_by_id( get_relationship_by_id.metadata = {'url': '/digitaltwins/{id}/relationships/{relationshipId}'} def add_relationship( - self, id, relationship_id, relationship=None, if_none_match=None, custom_headers=None, raw=False, **operation_config): + self, relationship, id, relationship_id, digital_twins_add_relationship_options=None, custom_headers=None, raw=False, **operation_config): """Adds a relationship between two digital twins. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is either no digital twin, target digital twin, - or relationship with the provided id. - 409 (Conflict): A relationship with the provided id already exists. + * 200 OK + * 400 Bad Request + * InvalidArgument - The digital twin id, relationship id, or payload is + invalid. + * InvalidRelationship - The relationship is invalid. + * OperationNotAllowed - The relationship cannot connect to the same + digital twin. + * ValidationFailed - The relationship content is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. + * TargetTwinNotFound - The digital twin target of the relationship was + not found. + * 412 Precondition Failed + * PreconditionFailed - The precondition check (If-Match or + If-None-Match) failed. + :param relationship: The data for the relationship. + :type relationship: object :param id: The id of the digital twin. The id is unique within the service and case sensitive. :type id: str :param relationship_id: The id of the relationship. The id is unique within the digital twin and case sensitive. :type relationship_id: str - :param relationship: The data for the relationship. - :type relationship: object - :param if_none_match: Only perform the operation if the entity does - not already exist. Possible values include: '*' - :type if_none_match: str + :param digital_twins_add_relationship_options: Additional parameters + for the operation + :type digital_twins_add_relationship_options: + ~digitaltwins.models.DigitalTwinsAddRelationshipOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -409,6 +522,16 @@ def add_relationship( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_add_relationship_options is not None: + traceparent = digital_twins_add_relationship_options.traceparent + tracestate = None + if digital_twins_add_relationship_options is not None: + tracestate = digital_twins_add_relationship_options.tracestate + if_none_match = None + if digital_twins_add_relationship_options is not None: + if_none_match = digital_twins_add_relationship_options.if_none_match + # Construct URL url = self.add_relationship.metadata['url'] path_format_arguments = { @@ -429,16 +552,17 @@ def add_relationship( header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - if if_none_match is not None: - header_parameters['If-None-Match'] = self._serialize.header("if_none_match", if_none_match, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if if_none_match is not None: + header_parameters['If-None-Match'] = self._serialize.header("if_none_match", if_none_match, 'str') # Construct body - if relationship is not None: - body_content = self._serialize.body(relationship, 'object') - else: - body_content = None + body_content = self._serialize.body(relationship, 'object') # Construct and send request request = self._client.put(url, query_parameters, header_parameters, body_content) @@ -465,12 +589,18 @@ def add_relationship( add_relationship.metadata = {'url': '/digitaltwins/{id}/relationships/{relationshipId}'} def delete_relationship( - self, id, relationship_id, if_match=None, custom_headers=None, raw=False, **operation_config): + self, id, relationship_id, digital_twins_delete_relationship_options=None, custom_headers=None, raw=False, **operation_config): """Deletes a relationship between two digital twins. Status codes: - 200 (OK): Success. - 404 (Not Found): There is either no digital twin or relationship with - the provided id. + * 204 No Content + * 400 Bad Request + * InvalidArgument - The digital twin id or relationship id is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. + * RelationshipNotFound - The relationship was not found. + * 412 Precondition Failed + * PreconditionFailed - The precondition check (If-Match or + If-None-Match) failed. :param id: The id of the digital twin. The id is unique within the service and case sensitive. @@ -478,9 +608,10 @@ def delete_relationship( :param relationship_id: The id of the relationship. The id is unique within the digital twin and case sensitive. :type relationship_id: str - :param if_match: Only perform the operation if the entity's etag - matches one of the etags provided or * is provided. - :type if_match: str + :param digital_twins_delete_relationship_options: Additional + parameters for the operation + :type digital_twins_delete_relationship_options: + ~digitaltwins.models.DigitalTwinsDeleteRelationshipOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -491,6 +622,16 @@ def delete_relationship( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_delete_relationship_options is not None: + traceparent = digital_twins_delete_relationship_options.traceparent + tracestate = None + if digital_twins_delete_relationship_options is not None: + tracestate = digital_twins_delete_relationship_options.tracestate + if_match = None + if digital_twins_delete_relationship_options is not None: + if_match = digital_twins_delete_relationship_options.if_match + # Construct URL url = self.delete_relationship.metadata['url'] path_format_arguments = { @@ -509,10 +650,14 @@ def delete_relationship( header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') # Construct and send request request = self._client.delete(url, query_parameters, header_parameters) @@ -527,26 +672,37 @@ def delete_relationship( delete_relationship.metadata = {'url': '/digitaltwins/{id}/relationships/{relationshipId}'} def update_relationship( - self, id, relationship_id, patch_document=None, if_match=None, custom_headers=None, raw=False, **operation_config): + self, patch_document, id, relationship_id, digital_twins_update_relationship_options=None, custom_headers=None, raw=False, **operation_config): """Updates the properties on a relationship between two digital twins. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is either no digital twin or relationship with - the provided id. + * 204 No Content + * 400 Bad Request + * InvalidArgument - The digital twin id or relationship id is invalid. + * InvalidRelationship - The relationship is invalid. + * JsonPatchInvalid - The JSON Patch provided is invalid. + * ValidationFailed - The relationship content is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. + * RelationshipNotFound - The relationship was not found. + * 409 Conflict + * RelationshipAlreadyExists - The relationship already exists. + * 412 Precondition Failed + * PreconditionFailed - The precondition check (If-Match or + If-None-Match) failed. + :param patch_document: JSON Patch description of the update to the + relationship properties. + :type patch_document: list[object] :param id: The id of the digital twin. The id is unique within the service and case sensitive. :type id: str :param relationship_id: The id of the relationship. The id is unique within the digital twin and case sensitive. :type relationship_id: str - :param patch_document: JSON Patch description of the update to the - relationship properties. - :type patch_document: list[object] - :param if_match: Only perform the operation if the entity's etag - matches one of the etags provided or * is provided. - :type if_match: str + :param digital_twins_update_relationship_options: Additional + parameters for the operation + :type digital_twins_update_relationship_options: + ~digitaltwins.models.DigitalTwinsUpdateRelationshipOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -557,6 +713,16 @@ def update_relationship( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_update_relationship_options is not None: + traceparent = digital_twins_update_relationship_options.traceparent + tracestate = None + if digital_twins_update_relationship_options is not None: + tracestate = digital_twins_update_relationship_options.tracestate + if_match = None + if digital_twins_update_relationship_options is not None: + if_match = digital_twins_update_relationship_options.if_match + # Construct URL url = self.update_relationship.metadata['url'] path_format_arguments = { @@ -576,16 +742,17 @@ def update_relationship( header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') # Construct body - if patch_document is not None: - body_content = self._serialize.body(patch_document, '[object]') - else: - body_content = None + body_content = self._serialize.body(patch_document, '[object]') # Construct and send request request = self._client.patch(url, query_parameters, header_parameters, body_content) @@ -603,18 +770,24 @@ def update_relationship( update_relationship.metadata = {'url': '/digitaltwins/{id}/relationships/{relationshipId}'} def list_relationships( - self, id, relationship_name=None, custom_headers=None, raw=False, **operation_config): + self, id, relationship_name=None, digital_twins_list_relationships_options=None, custom_headers=None, raw=False, **operation_config): """Retrieves the relationships from a digital twin. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is no digital twin with the provided id. + * 200 OK + * 400 Bad Request + * InvalidArgument - The digital twin id is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. :param id: The id of the digital twin. The id is unique within the service and case sensitive. :type id: str :param relationship_name: The name of the relationship. :type relationship_name: str + :param digital_twins_list_relationships_options: Additional parameters + for the operation + :type digital_twins_list_relationships_options: + ~digitaltwins.models.DigitalTwinsListRelationshipsOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -625,6 +798,13 @@ def list_relationships( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_list_relationships_options is not None: + traceparent = digital_twins_list_relationships_options.traceparent + tracestate = None + if digital_twins_list_relationships_options is not None: + tracestate = digital_twins_list_relationships_options.tracestate + def internal_paging(next_link=None, raw=False): if not next_link: @@ -654,6 +834,10 @@ def internal_paging(next_link=None, raw=False): header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -676,16 +860,22 @@ def internal_paging(next_link=None, raw=False): list_relationships.metadata = {'url': '/digitaltwins/{id}/relationships'} def list_incoming_relationships( - self, id, custom_headers=None, raw=False, **operation_config): + self, id, digital_twins_list_incoming_relationships_options=None, custom_headers=None, raw=False, **operation_config): """Retrieves all incoming relationship for a digital twin. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is no digital twin with the provided id. + * 200 OK + * 400 Bad Request + * InvalidArgument - The digital twin id is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. :param id: The id of the digital twin. The id is unique within the service and case sensitive. :type id: str + :param digital_twins_list_incoming_relationships_options: Additional + parameters for the operation + :type digital_twins_list_incoming_relationships_options: + ~digitaltwins.models.DigitalTwinsListIncomingRelationshipsOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -697,6 +887,13 @@ def list_incoming_relationships( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_list_incoming_relationships_options is not None: + traceparent = digital_twins_list_incoming_relationships_options.traceparent + tracestate = None + if digital_twins_list_incoming_relationships_options is not None: + tracestate = digital_twins_list_incoming_relationships_options.tracestate + def internal_paging(next_link=None, raw=False): if not next_link: @@ -724,6 +921,10 @@ def internal_paging(next_link=None, raw=False): header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -746,25 +947,32 @@ def internal_paging(next_link=None, raw=False): list_incoming_relationships.metadata = {'url': '/digitaltwins/{id}/incomingrelationships'} def send_telemetry( - self, id, telemetry, dt_id, dt_timestamp=None, custom_headers=None, raw=False, **operation_config): + self, telemetry, id, message_id, telemetry_source_time=None, digital_twins_send_telemetry_options=None, custom_headers=None, raw=False, **operation_config): """Sends telemetry on behalf of a digital twin. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is no digital twin with the provided id. + * 204 No Content + * 400 Bad Request + * InvalidArgument - The digital twin id or message id is invalid. + * ValidationFailed - The telemetry content is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. - :param id: The id of the digital twin. The id is unique within the - service and case sensitive. - :type id: str :param telemetry: The telemetry measurements to send from the digital twin. :type telemetry: object - :param dt_id: A unique message identifier (in the scope of the digital - twin id) that is commonly used for de-duplicating messages. - :type dt_id: str - :param dt_timestamp: An RFC 3339 timestamp that identifies the time - the telemetry was measured. - :type dt_timestamp: str + :param id: The id of the digital twin. The id is unique within the + service and case sensitive. + :type id: str + :param message_id: A unique message identifier (in the scope of the + digital twin id) that is commonly used for de-duplicating messages. + :type message_id: str + :param telemetry_source_time: An RFC 3339 timestamp that identifies + the time the telemetry was measured. + :type telemetry_source_time: str + :param digital_twins_send_telemetry_options: Additional parameters for + the operation + :type digital_twins_send_telemetry_options: + ~digitaltwins.models.DigitalTwinsSendTelemetryOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -775,6 +983,13 @@ def send_telemetry( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_send_telemetry_options is not None: + traceparent = digital_twins_send_telemetry_options.traceparent + tracestate = None + if digital_twins_send_telemetry_options is not None: + tracestate = digital_twins_send_telemetry_options.tracestate + # Construct URL url = self.send_telemetry.metadata['url'] path_format_arguments = { @@ -784,8 +999,6 @@ def send_telemetry( # Construct parameters query_parameters = {} - - # @digimaun - add api-version query parameter query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') # Construct headers @@ -795,11 +1008,15 @@ def send_telemetry( header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - header_parameters['dt-id'] = self._serialize.header("dt_id", dt_id, 'str') - if dt_timestamp is not None: - header_parameters['dt-timestamp'] = self._serialize.header("dt_timestamp", dt_timestamp, 'str') + header_parameters['Message-Id'] = self._serialize.header("message_id", message_id, 'str') + if telemetry_source_time is not None: + header_parameters['Telemetry-Source-Time'] = self._serialize.header("telemetry_source_time", telemetry_source_time, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct body body_content = self._serialize.body(telemetry, 'object') @@ -817,28 +1034,36 @@ def send_telemetry( send_telemetry.metadata = {'url': '/digitaltwins/{id}/telemetry'} def send_component_telemetry( - self, id, component_path, telemetry, dt_id, dt_timestamp=None, custom_headers=None, raw=False, **operation_config): + self, telemetry, id, component_path, message_id, telemetry_source_time=None, digital_twins_send_component_telemetry_options=None, custom_headers=None, raw=False, **operation_config): """Sends telemetry on behalf of a component in a digital twin. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is either no digital twin with the provided id - or the component path is invalid. + * 204 No Content + * 400 Bad Request + * InvalidArgument - The digital twin id, message id, or component path + is invalid. + * ValidationFailed - The telemetry content is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. + * ComponentNotFound - The component path was not found. + :param telemetry: The telemetry measurements to send from the digital + twin's component. + :type telemetry: object :param id: The id of the digital twin. The id is unique within the service and case sensitive. :type id: str :param component_path: The name of the DTDL component. :type component_path: str - :param telemetry: The telemetry measurements to send from the digital - twin's component. - :type telemetry: object - :param dt_id: A unique message identifier (in the scope of the digital - twin id) that is commonly used for de-duplicating messages. - :type dt_id: str - :param dt_timestamp: An RFC 3339 timestamp that identifies the time - the telemetry was measured. - :type dt_timestamp: str + :param message_id: A unique message identifier (in the scope of the + digital twin id) that is commonly used for de-duplicating messages. + :type message_id: str + :param telemetry_source_time: An RFC 3339 timestamp that identifies + the time the telemetry was measured. + :type telemetry_source_time: str + :param digital_twins_send_component_telemetry_options: Additional + parameters for the operation + :type digital_twins_send_component_telemetry_options: + ~digitaltwins.models.DigitalTwinsSendComponentTelemetryOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -849,6 +1074,13 @@ def send_component_telemetry( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_send_component_telemetry_options is not None: + traceparent = digital_twins_send_component_telemetry_options.traceparent + tracestate = None + if digital_twins_send_component_telemetry_options is not None: + tracestate = digital_twins_send_component_telemetry_options.tracestate + # Construct URL url = self.send_component_telemetry.metadata['url'] path_format_arguments = { @@ -859,8 +1091,6 @@ def send_component_telemetry( # Construct parameters query_parameters = {} - - # @digimaun - add api-version query parameter query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') # Construct headers @@ -870,11 +1100,15 @@ def send_component_telemetry( header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - header_parameters['dt-id'] = self._serialize.header("dt_id", dt_id, 'str') - if dt_timestamp is not None: - header_parameters['dt-timestamp'] = self._serialize.header("dt_timestamp", dt_timestamp, 'str') + header_parameters['Message-Id'] = self._serialize.header("message_id", message_id, 'str') + if telemetry_source_time is not None: + header_parameters['Telemetry-Source-Time'] = self._serialize.header("telemetry_source_time", telemetry_source_time, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct body body_content = self._serialize.body(telemetry, 'object') @@ -892,18 +1126,25 @@ def send_component_telemetry( send_component_telemetry.metadata = {'url': '/digitaltwins/{id}/components/{componentPath}/telemetry'} def get_component( - self, id, component_path, custom_headers=None, raw=False, **operation_config): + self, id, component_path, digital_twins_get_component_options=None, custom_headers=None, raw=False, **operation_config): """Retrieves a component from a digital twin. Status codes: - 200 (OK): Success. - 404 (Not Found): There is either no digital twin with the provided id - or the component path is invalid. + * 200 OK + * 400 Bad Request + * InvalidArgument - The digital twin id or component path is invalid. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. + * ComponentNotFound - The component path was not found. :param id: The id of the digital twin. The id is unique within the service and case sensitive. :type id: str :param component_path: The name of the DTDL component. :type component_path: str + :param digital_twins_get_component_options: Additional parameters for + the operation + :type digital_twins_get_component_options: + ~digitaltwins.models.DigitalTwinsGetComponentOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -914,6 +1155,13 @@ def get_component( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_get_component_options is not None: + traceparent = digital_twins_get_component_options.traceparent + tracestate = None + if digital_twins_get_component_options is not None: + tracestate = digital_twins_get_component_options.tracestate + # Construct URL url = self.get_component.metadata['url'] path_format_arguments = { @@ -935,6 +1183,10 @@ def get_component( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -961,26 +1213,35 @@ def get_component( get_component.metadata = {'url': '/digitaltwins/{id}/components/{componentPath}'} def update_component( - self, id, component_path, patch_document=None, if_match=None, custom_headers=None, raw=False, **operation_config): + self, patch_document, id, component_path, digital_twins_update_component_options=None, custom_headers=None, raw=False, **operation_config): """Updates a component on a digital twin. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. - 404 (Not Found): There is either no digital twin with the provided id - or the component path is invalid. + * 204 No Content + * 400 Bad Request + * InvalidArgument - The digital twin id, component path, or payload is + invalid. + * JsonPatchInvalid - The JSON Patch provided is invalid. + * ValidationFailed - Applying the patch results in an invalid digital + twin. + * 404 Not Found + * DigitalTwinNotFound - The digital twin was not found. + * 412 Precondition Failed + * PreconditionFailed - The precondition check (If-Match or + If-None-Match) failed. + :param patch_document: An update specification described by JSON + Patch. Updates to property values and $model elements may happen in + the same request. Operations are limited to add, replace and remove. + :type patch_document: list[object] :param id: The id of the digital twin. The id is unique within the service and case sensitive. :type id: str :param component_path: The name of the DTDL component. :type component_path: str - :param patch_document: An update specification described by JSON - Patch. Updates to property values and $model elements may happen in - the same request. Operations are limited to add, replace and remove. - :type patch_document: list[object] - :param if_match: Only perform the operation if the entity's etag - matches one of the etags provided or * is provided. - :type if_match: str + :param digital_twins_update_component_options: Additional parameters + for the operation + :type digital_twins_update_component_options: + ~digitaltwins.models.DigitalTwinsUpdateComponentOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -991,6 +1252,16 @@ def update_component( :raises: :class:`ErrorResponseException` """ + traceparent = None + if digital_twins_update_component_options is not None: + traceparent = digital_twins_update_component_options.traceparent + tracestate = None + if digital_twins_update_component_options is not None: + tracestate = digital_twins_update_component_options.tracestate + if_match = None + if digital_twins_update_component_options is not None: + if_match = digital_twins_update_component_options.if_match + # Construct URL url = self.update_component.metadata['url'] path_format_arguments = { @@ -1010,16 +1281,17 @@ def update_component( header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') # Construct body - if patch_document is not None: - body_content = self._serialize.body(patch_document, '[object]') - else: - body_content = None + body_content = self._serialize.body(patch_document, '[object]') # Construct and send request request = self._client.patch(url, query_parameters, header_parameters, body_content) diff --git a/azext_iot/sdk/digitaltwins/operations/event_routes_operations.py b/azext_iot/sdk/digitaltwins/dataplane/operations/event_routes_operations.py similarity index 71% rename from azext_iot/sdk/digitaltwins/operations/event_routes_operations.py rename to azext_iot/sdk/digitaltwins/dataplane/operations/event_routes_operations.py index 2969ce3a1..83d6275cd 100644 --- a/azext_iot/sdk/digitaltwins/operations/event_routes_operations.py +++ b/azext_iot/sdk/digitaltwins/dataplane/operations/event_routes_operations.py @@ -22,7 +22,7 @@ class EventRoutesOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: The requested API version. Constant value: "2020-05-31-preview". + :ivar api_version: The API version to use for the request. Constant value: "2020-10-31". """ models = models @@ -32,7 +32,7 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-05-31-preview" + self.api_version = "2020-10-31" self.config = config @@ -40,8 +40,7 @@ def list( self, event_routes_list_options=None, custom_headers=None, raw=False, **operation_config): """Retrieves all event routes. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. + * 200 OK. :param event_routes_list_options: Additional parameters for the operation @@ -58,9 +57,15 @@ def list( :raises: :class:`ErrorResponseException` """ - max_item_count = None + traceparent = None if event_routes_list_options is not None: - max_item_count = event_routes_list_options.max_item_count + traceparent = event_routes_list_options.traceparent + tracestate = None + if event_routes_list_options is not None: + tracestate = event_routes_list_options.tracestate + max_items_per_page = None + if event_routes_list_options is not None: + max_items_per_page = event_routes_list_options.max_items_per_page def internal_paging(next_link=None, raw=False): @@ -85,8 +90,12 @@ def internal_paging(next_link=None, raw=False): header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - if max_item_count is not None: - header_parameters['x-ms-max-item-count'] = self._serialize.header("max_item_count", max_item_count, 'int') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if max_items_per_page is not None: + header_parameters['max-items-per-page'] = self._serialize.header("max_items_per_page", max_items_per_page, 'int') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -109,15 +118,20 @@ def internal_paging(next_link=None, raw=False): list.metadata = {'url': '/eventroutes'} def get_by_id( - self, id, custom_headers=None, raw=False, **operation_config): + self, id, event_routes_get_by_id_options=None, custom_headers=None, raw=False, **operation_config): """Retrieves an event route. Status codes: - 200 (OK): Success. - 404 (Not Found): There is no event route with the provided id. + * 200 OK + * 404 Not Found + * EventRouteNotFound - The event route was not found. :param id: The id for an event route. The id is unique within event routes and case sensitive. :type id: str + :param event_routes_get_by_id_options: Additional parameters for the + operation + :type event_routes_get_by_id_options: + ~digitaltwins.models.EventRoutesGetByIdOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -129,6 +143,13 @@ def get_by_id( :raises: :class:`ErrorResponseException` """ + traceparent = None + if event_routes_get_by_id_options is not None: + traceparent = event_routes_get_by_id_options.traceparent + tracestate = None + if event_routes_get_by_id_options is not None: + tracestate = event_routes_get_by_id_options.tracestate + # Construct URL url = self.get_by_id.metadata['url'] path_format_arguments = { @@ -149,6 +170,10 @@ def get_by_id( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -170,21 +195,31 @@ def get_by_id( get_by_id.metadata = {'url': '/eventroutes/{id}'} def add( - self, id, endpoint_id, filter=None, custom_headers=None, raw=False, **operation_config): + self, id, endpoint_name, filter, event_routes_add_options=None, custom_headers=None, raw=False, **operation_config): """Adds or replaces an event route. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. + * 204 No Content + * 400 Bad Request + * EventRouteEndpointInvalid - The endpoint provided does not exist or + is not active. + * EventRouteFilterInvalid - The event route filter is invalid. + * EventRouteIdInvalid - The event route id is invalid. + * LimitExceeded - The maximum number of event routes allowed has been + reached. :param id: The id for an event route. The id is unique within event routes and case sensitive. :type id: str - :param endpoint_id: The id of the endpoint this event route is bound - to. - :type endpoint_id: str + :param endpoint_name: The name of the endpoint this event route is + bound to. + :type endpoint_name: str :param filter: An expression which describes the events which are routed to the endpoint. :type filter: str + :param event_routes_add_options: Additional parameters for the + operation + :type event_routes_add_options: + ~digitaltwins.models.EventRoutesAddOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -195,10 +230,15 @@ def add( :raises: :class:`ErrorResponseException` """ + traceparent = None + if event_routes_add_options is not None: + traceparent = event_routes_add_options.traceparent + tracestate = None + if event_routes_add_options is not None: + tracestate = event_routes_add_options.tracestate event_route = None - if endpoint_id is not None or filter is not None: - # @digimaun - custom change related to endpointId -> endpointName - event_route = models.EventRoute(endpoint_name=endpoint_id, filter=filter) + if endpoint_name is not None or filter is not None: + event_route = models.EventRoute(endpoint_name=endpoint_name, filter=filter) # Construct URL url = self.add.metadata['url'] @@ -220,6 +260,10 @@ def add( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct body if event_route is not None: @@ -240,15 +284,20 @@ def add( add.metadata = {'url': '/eventroutes/{id}'} def delete( - self, id, custom_headers=None, raw=False, **operation_config): + self, id, event_routes_delete_options=None, custom_headers=None, raw=False, **operation_config): """Deletes an event route. Status codes: - 200 (OK): Success. - 404 (Not Found): There is no event route with the provided id. + * 204 No Content + * 404 Not Found + * EventRouteNotFound - The event route was not found. :param id: The id for an event route. The id is unique within event routes and case sensitive. :type id: str + :param event_routes_delete_options: Additional parameters for the + operation + :type event_routes_delete_options: + ~digitaltwins.models.EventRoutesDeleteOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -259,6 +308,13 @@ def delete( :raises: :class:`ErrorResponseException` """ + traceparent = None + if event_routes_delete_options is not None: + traceparent = event_routes_delete_options.traceparent + tracestate = None + if event_routes_delete_options is not None: + tracestate = event_routes_delete_options.tracestate + # Construct URL url = self.delete.metadata['url'] path_format_arguments = { @@ -278,6 +334,10 @@ def delete( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') # Construct and send request request = self._client.delete(url, query_parameters, header_parameters) diff --git a/azext_iot/sdk/digitaltwins/operations/query_operations.py b/azext_iot/sdk/digitaltwins/dataplane/operations/query_operations.py similarity index 70% rename from azext_iot/sdk/digitaltwins/operations/query_operations.py rename to azext_iot/sdk/digitaltwins/dataplane/operations/query_operations.py index 9c89d5f62..5da512a4b 100644 --- a/azext_iot/sdk/digitaltwins/operations/query_operations.py +++ b/azext_iot/sdk/digitaltwins/dataplane/operations/query_operations.py @@ -22,7 +22,7 @@ class QueryOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: The requested API version. Constant value: "2020-05-31-preview". + :ivar api_version: The API version to use for the request. Constant value: "2020-10-31". """ models = models @@ -32,17 +32,21 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-05-31-preview" + self.api_version = "2020-10-31" self.config = config def query_twins( - self, query=None, continuation_token=None, custom_headers=None, raw=False, **operation_config): + self, query=None, continuation_token=None, query_query_twins_options=None, custom_headers=None, raw=False, **operation_config): """Executes a query that allows traversing relationships and filtering by property values. Status codes: - 200 (OK): Success. - 400 (Bad Request): The request is invalid. + * 200 OK + * 400 Bad Request + * BadRequest - The continuation token is invalid. + * SqlQueryError - The query contains some errors. + * 429 Too Many Requests + * QuotaReachedError - The maximum query rate limit has been reached. :param query: The query to execute. This value is ignored if a continuation token is provided. @@ -50,6 +54,10 @@ def query_twins( :param continuation_token: A token which is used to retrieve the next set of results from a previous query. :type continuation_token: str + :param query_query_twins_options: Additional parameters for the + operation + :type query_query_twins_options: + ~digitaltwins.models.QueryQueryTwinsOptions :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -61,6 +69,15 @@ def query_twins( :raises: :class:`ErrorResponseException` """ + traceparent = None + if query_query_twins_options is not None: + traceparent = query_query_twins_options.traceparent + tracestate = None + if query_query_twins_options is not None: + tracestate = query_query_twins_options.tracestate + max_items_per_page = None + if query_query_twins_options is not None: + max_items_per_page = query_query_twins_options.max_items_per_page query_specification = models.QuerySpecification(query=query, continuation_token=continuation_token) # Construct URL @@ -80,6 +97,12 @@ def query_twins( header_parameters.update(custom_headers) if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + if traceparent is not None: + header_parameters['traceparent'] = self._serialize.header("traceparent", traceparent, 'str') + if tracestate is not None: + header_parameters['tracestate'] = self._serialize.header("tracestate", tracestate, 'str') + if max_items_per_page is not None: + header_parameters['max-items-per-page'] = self._serialize.header("max_items_per_page", max_items_per_page, 'int') # Construct body body_content = self._serialize.body(query_specification, 'QuerySpecification') @@ -97,7 +120,7 @@ def query_twins( if response.status_code == 200: deserialized = self._deserialize('QueryResult', response) header_dict = { - 'query-charge': 'str', + 'query-charge': 'float', } if raw: diff --git a/azext_iot/sdk/digitaltwins_arm/version.py b/azext_iot/sdk/digitaltwins/dataplane/version.py similarity index 93% rename from azext_iot/sdk/digitaltwins_arm/version.py rename to azext_iot/sdk/digitaltwins/dataplane/version.py index be153c187..ac2482d2d 100644 --- a/azext_iot/sdk/digitaltwins_arm/version.py +++ b/azext_iot/sdk/digitaltwins/dataplane/version.py @@ -9,4 +9,5 @@ # regenerated. # -------------------------------------------------------------------------- -VERSION = "2020-03-01-preview" +VERSION = "2020-10-31" + diff --git a/azext_iot/sdk/digitaltwins/models/__init__.py b/azext_iot/sdk/digitaltwins/models/__init__.py deleted file mode 100644 index 62848fc83..000000000 --- a/azext_iot/sdk/digitaltwins/models/__init__.py +++ /dev/null @@ -1,54 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -try: - from .event_route_py3 import EventRoute - from .model_data_py3 import ModelData - from .incoming_relationship_py3 import IncomingRelationship - from .query_specification_py3 import QuerySpecification - from .query_result_py3 import QueryResult - from .inner_error_py3 import InnerError - from .error_py3 import Error - from .error_response_py3 import ErrorResponse, ErrorResponseException - from .digital_twin_models_list_options_py3 import DigitalTwinModelsListOptions - from .event_routes_list_options_py3 import EventRoutesListOptions -except (SyntaxError, ImportError): - from .event_route import EventRoute - from .model_data import ModelData - from .incoming_relationship import IncomingRelationship - from .query_specification import QuerySpecification - from .query_result import QueryResult - from .inner_error import InnerError - from .error import Error - from .error_response import ErrorResponse, ErrorResponseException - from .digital_twin_models_list_options import DigitalTwinModelsListOptions - from .event_routes_list_options import EventRoutesListOptions -from .model_data_paged import ModelDataPaged -from .object_paged import ObjectPaged -from .incoming_relationship_paged import IncomingRelationshipPaged -from .event_route_paged import EventRoutePaged - -__all__ = [ - 'EventRoute', - 'ModelData', - 'IncomingRelationship', - 'QuerySpecification', - 'QueryResult', - 'InnerError', - 'Error', - 'ErrorResponse', 'ErrorResponseException', - 'DigitalTwinModelsListOptions', - 'EventRoutesListOptions', - 'ModelDataPaged', - 'ObjectPaged', - 'IncomingRelationshipPaged', - 'EventRoutePaged', -] diff --git a/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options_py3.py b/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options_py3.py deleted file mode 100644 index 56a13018e..000000000 --- a/azext_iot/sdk/digitaltwins/models/digital_twin_models_list_options_py3.py +++ /dev/null @@ -1,30 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinModelsListOptions(Model): - """Additional parameters for list operation. - - :param max_item_count: The maximum number of items to retrieve per - request. The server may choose to return less than the requested max. - Default value: -1 . - :type max_item_count: int - """ - - _attribute_map = { - 'max_item_count': {'key': '', 'type': 'int'}, - } - - def __init__(self, *, max_item_count: int=-1, **kwargs) -> None: - super(DigitalTwinModelsListOptions, self).__init__(**kwargs) - self.max_item_count = max_item_count diff --git a/azext_iot/sdk/digitaltwins/models/event_routes_list_options_py3.py b/azext_iot/sdk/digitaltwins/models/event_routes_list_options_py3.py deleted file mode 100644 index 9e17db02b..000000000 --- a/azext_iot/sdk/digitaltwins/models/event_routes_list_options_py3.py +++ /dev/null @@ -1,30 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class EventRoutesListOptions(Model): - """Additional parameters for list operation. - - :param max_item_count: The maximum number of items to retrieve per - request. The server may choose to return less than the requested max. - Default value: -1 . - :type max_item_count: int - """ - - _attribute_map = { - 'max_item_count': {'key': '', 'type': 'int'}, - } - - def __init__(self, *, max_item_count: int=-1, **kwargs) -> None: - super(EventRoutesListOptions, self).__init__(**kwargs) - self.max_item_count = max_item_count diff --git a/azext_iot/sdk/digitaltwins/models/model_data_paged.py b/azext_iot/sdk/digitaltwins/models/model_data_paged.py deleted file mode 100644 index b36b45be4..000000000 --- a/azext_iot/sdk/digitaltwins/models/model_data_paged.py +++ /dev/null @@ -1,28 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.paging import Paged - - -class ModelDataPaged(Paged): - """ - A paging container for iterating over a list of :class:`ModelData ` object - """ - - # @digimaun - [ModelData] -> [object] - _attribute_map = { - 'next_link': {'key': 'nextLink', 'type': 'str'}, - 'current_page': {'key': 'value', 'type': '[object]'} - } - - def __init__(self, *args, **kwargs): - - super(ModelDataPaged, self).__init__(*args, **kwargs) diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource.py deleted file mode 100644 index d2abcd93b..000000000 --- a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource.py +++ /dev/null @@ -1,59 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .external_resource import ExternalResource - - -class DigitalTwinsEndpointResource(ExternalResource): - """DigitalTwinsInstance endpoint resource. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar id: The resource identifier. - :vartype id: str - :ivar name: Extension resource name. - :vartype name: str - :ivar type: The resource type. - :vartype type: str - :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' - :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState - :ivar created_time: Time when the Endpoint was added to - DigitalTwinsInstance. - :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] - """ - - _validation = { - 'id': {'readonly': True}, - 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, - 'type': {'readonly': True}, - 'provisioning_state': {'readonly': True}, - 'created_time': {'readonly': True}, - } - - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'name': {'key': 'name', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'str'}, - 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, - 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'properties.tags', 'type': '{str}'}, - } - - def __init__(self, **kwargs): - super(DigitalTwinsEndpointResource, self).__init__(**kwargs) - self.provisioning_state = None - self.created_time = None - self.tags = kwargs.get('tags', None) diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_py3.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_py3.py deleted file mode 100644 index fdb82c623..000000000 --- a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_endpoint_resource_py3.py +++ /dev/null @@ -1,59 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .external_resource_py3 import ExternalResource - - -class DigitalTwinsEndpointResource(ExternalResource): - """DigitalTwinsInstance endpoint resource. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar id: The resource identifier. - :vartype id: str - :ivar name: Extension resource name. - :vartype name: str - :ivar type: The resource type. - :vartype type: str - :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled' - :vartype provisioning_state: str or - ~digitaltwins-arm.models.EndpointProvisioningState - :ivar created_time: Time when the Endpoint was added to - DigitalTwinsInstance. - :vartype created_time: datetime - :param tags: The resource tags. - :type tags: dict[str, str] - """ - - _validation = { - 'id': {'readonly': True}, - 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, - 'type': {'readonly': True}, - 'provisioning_state': {'readonly': True}, - 'created_time': {'readonly': True}, - } - - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'name': {'key': 'name', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'str'}, - 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, - 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, - 'tags': {'key': 'properties.tags', 'type': '{str}'}, - } - - def __init__(self, *, tags=None, **kwargs) -> None: - super(DigitalTwinsEndpointResource, self).__init__(**kwargs) - self.provisioning_state = None - self.created_time = None - self.tags = tags diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info.py deleted file mode 100644 index ac355d53c..000000000 --- a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info.py +++ /dev/null @@ -1,35 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinsSkuInfo(Model): - """Information about the SKU of the DigitalTwinsInstance. - - Variables are only populated by the server, and will be ignored when - sending a request. - - All required parameters must be populated in order to send to Azure. - - :ivar name: Required. The name of the SKU. Default value: "F1" . - :vartype name: str - """ - - _validation = { - 'name': {'required': True, 'constant': True}, - } - - _attribute_map = { - 'name': {'key': 'name', 'type': 'str'}, - } - - name = "F1" diff --git a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info_py3.py b/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info_py3.py deleted file mode 100644 index ac355d53c..000000000 --- a/azext_iot/sdk/digitaltwins_arm/models/digital_twins_sku_info_py3.py +++ /dev/null @@ -1,35 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinsSkuInfo(Model): - """Information about the SKU of the DigitalTwinsInstance. - - Variables are only populated by the server, and will be ignored when - sending a request. - - All required parameters must be populated in order to send to Azure. - - :ivar name: Required. The name of the SKU. Default value: "F1" . - :vartype name: str - """ - - _validation = { - 'name': {'required': True, 'constant': True}, - } - - _attribute_map = { - 'name': {'key': 'name', 'type': 'str'}, - } - - name = "F1" diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource.py deleted file mode 100644 index 568b35915..000000000 --- a/azext_iot/sdk/digitaltwins_arm/models/integration_resource.py +++ /dev/null @@ -1,61 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .external_resource import ExternalResource - - -class IntegrationResource(ExternalResource): - """IoTHub integration resource. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar id: The resource identifier. - :vartype id: str - :ivar name: Extension resource name. - :vartype name: str - :ivar type: The resource type. - :vartype type: str - :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. - Possible values include: 'Provisioning', 'Deleting', 'Succeeded', - 'Failed', 'Canceled' - :vartype provisioning_state: str or - ~digitaltwins-arm.models.IntegrationResourceState - :param resource_id: Fully qualified resource identifier of the - DigitalTwins Azure resource. - :type resource_id: str - :ivar created_time: Time when the IoTHub was added to - DigitalTwinsInstance. - :vartype created_time: datetime - """ - - _validation = { - 'id': {'readonly': True}, - 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, - 'type': {'readonly': True}, - 'provisioning_state': {'readonly': True}, - 'created_time': {'readonly': True}, - } - - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'name': {'key': 'name', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'str'}, - 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, - 'resource_id': {'key': 'properties.resourceId', 'type': 'str'}, - 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, - } - - def __init__(self, **kwargs): - super(IntegrationResource, self).__init__(**kwargs) - self.provisioning_state = None - self.resource_id = kwargs.get('resource_id', None) - self.created_time = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_py3.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_py3.py deleted file mode 100644 index 29d42c445..000000000 --- a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_py3.py +++ /dev/null @@ -1,61 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .external_resource_py3 import ExternalResource - - -class IntegrationResource(ExternalResource): - """IoTHub integration resource. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar id: The resource identifier. - :vartype id: str - :ivar name: Extension resource name. - :vartype name: str - :ivar type: The resource type. - :vartype type: str - :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. - Possible values include: 'Provisioning', 'Deleting', 'Succeeded', - 'Failed', 'Canceled' - :vartype provisioning_state: str or - ~digitaltwins-arm.models.IntegrationResourceState - :param resource_id: Fully qualified resource identifier of the - DigitalTwins Azure resource. - :type resource_id: str - :ivar created_time: Time when the IoTHub was added to - DigitalTwinsInstance. - :vartype created_time: datetime - """ - - _validation = { - 'id': {'readonly': True}, - 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, - 'type': {'readonly': True}, - 'provisioning_state': {'readonly': True}, - 'created_time': {'readonly': True}, - } - - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'name': {'key': 'name', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'str'}, - 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, - 'resource_id': {'key': 'properties.resourceId', 'type': 'str'}, - 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, - } - - def __init__(self, *, resource_id: str=None, **kwargs) -> None: - super(IntegrationResource, self).__init__(**kwargs) - self.provisioning_state = None - self.resource_id = resource_id - self.created_time = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1.py deleted file mode 100644 index d822089c9..000000000 --- a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1.py +++ /dev/null @@ -1,38 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class IntegrationResourceState1(Model): - """Properties related to the IoTHub DigitalTwinsInstance Integration Resource. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. - Possible values include: 'Provisioning', 'Deleting', 'Succeeded', - 'Failed', 'Canceled' - :vartype provisioning_state: str or - ~digitaltwins-arm.models.IntegrationResourceState - """ - - _validation = { - 'provisioning_state': {'readonly': True}, - } - - _attribute_map = { - 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, - } - - def __init__(self, **kwargs): - super(IntegrationResourceState1, self).__init__(**kwargs) - self.provisioning_state = None diff --git a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1_py3.py b/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1_py3.py deleted file mode 100644 index c8dad07fb..000000000 --- a/azext_iot/sdk/digitaltwins_arm/models/integration_resource_state1_py3.py +++ /dev/null @@ -1,38 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class IntegrationResourceState1(Model): - """Properties related to the IoTHub DigitalTwinsInstance Integration Resource. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar provisioning_state: DigitalTwinsInstance - IoTHub link state. - Possible values include: 'Provisioning', 'Deleting', 'Succeeded', - 'Failed', 'Canceled' - :vartype provisioning_state: str or - ~digitaltwins-arm.models.IntegrationResourceState - """ - - _validation = { - 'provisioning_state': {'readonly': True}, - } - - _attribute_map = { - 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, - } - - def __init__(self, **kwargs) -> None: - super(IntegrationResourceState1, self).__init__(**kwargs) - self.provisioning_state = None diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py index c6b763a18..d11377b2b 100644 --- a/azext_iot/tests/digitaltwins/__init__.py +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -14,6 +14,7 @@ MOCK_RESOURCE_TAGS = "a=b;c=d" MOCK_ENDPOINT_TAGS = "key0=value0;key1=value1;" +MOCK_DEAD_LETTER_SECRET = 'https://accountname.blob.core.windows.net/containerName?sasToken' def generate_resource_id(): diff --git a/azext_iot/tests/digitaltwins/models/Floor.json b/azext_iot/tests/digitaltwins/models/Floor.json index 58969594d..2f1c00aa4 100644 --- a/azext_iot/tests/digitaltwins/models/Floor.json +++ b/azext_iot/tests/digitaltwins/models/Floor.json @@ -1,11 +1,11 @@ { - "@id": "dtmi:example:Floor;1", + "@id": "dtmi:com:example:Floor;1", "@type": "Interface", "@context": "dtmi:dtdl:context;2", "displayName": "Floor", "contents": [{ "@type": "Relationship", - "@id": "dtmi:contosocom:DigitalTwins:contains;1", + "target": "dtmi:com:example:Room;1", "name": "contains", "properties": [{ "@type": "Property", diff --git a/azext_iot/tests/digitaltwins/models/nested/Room.json b/azext_iot/tests/digitaltwins/models/nested/Room.json index 03ce6cdee..1df6992e2 100644 --- a/azext_iot/tests/digitaltwins/models/nested/Room.json +++ b/azext_iot/tests/digitaltwins/models/nested/Room.json @@ -1,5 +1,5 @@ { - "@id": "dtmi:example:Room;1", + "@id": "dtmi:com:example:Room;1", "@type": "Interface", "@context": "dtmi:dtdl:context;2", "displayName": "Room", diff --git a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py index ee62ac03e..cc8921118 100644 --- a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py @@ -30,7 +30,7 @@ def test_dt_models(self): models_directory = "./models" inline_model = "./models/Floor.json" component_dtmi = "dtmi:com:example:Thermostat;1" - room_dtmi = "dtmi:example:Room;1" + room_dtmi = "dtmi:com:example:Room;1" self.cmd( "dt create -n {} -g {} -l {}".format( @@ -45,7 +45,7 @@ def test_dt_models(self): ) # Wait for RBAC to catch-up - sleep(10) + sleep(15) create_models_output = self.cmd( "dt model create -n {} --from-directory '{}'".format( diff --git a/azext_iot/tests/digitaltwins/test_dt_provider_unit.py b/azext_iot/tests/digitaltwins/test_dt_provider_unit.py index 6eb851def..e6d7eb3b8 100644 --- a/azext_iot/tests/digitaltwins/test_dt_provider_unit.py +++ b/azext_iot/tests/digitaltwins/test_dt_provider_unit.py @@ -19,7 +19,7 @@ @pytest.fixture def get_mgmt_client(mocker, fixture_cmd): - from azext_iot.sdk.digitaltwins_arm import AzureDigitalTwinsManagementClient + from azext_iot.sdk.digitaltwins.controlplane import AzureDigitalTwinsManagementClient from azext_iot.digitaltwins.providers.auth import DigitalTwinAuthentication patched_get_raw_token = mocker.patch( 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 02569767b..373bb5f01 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -13,7 +13,7 @@ from . import DTLiveScenarioTest from . import ( MOCK_RESOURCE_TAGS, - MOCK_ENDPOINT_TAGS, + MOCK_DEAD_LETTER_SECRET, generate_resource_id, ) @@ -246,7 +246,7 @@ def test_dt_endpoints_routes(self): ) ) - sleep(4) # Wait for service to catch-up + sleep(10) # Wait for service to catch-up list_ep_output = self.cmd( "dt endpoint list -n {}".format(endpoints_instance_name) @@ -259,21 +259,19 @@ def test_dt_endpoints_routes(self): logger.debug("Adding eventgrid endpoint...") add_ep_output = self.cmd( - "dt endpoint create eventgrid -n {} -g {} --egg {} --egt {} --en {} --tags {}".format( + "dt endpoint create eventgrid -n {} -g {} --egg {} --egt {} --en {} --dsu {}".format( endpoints_instance_name, self.dt_resource_group, eventgrid_rg, eventgrid_topic, eventgrid_endpoint, - MOCK_ENDPOINT_TAGS, + MOCK_DEAD_LETTER_SECRET ) ).get_output_in_json() - assert_common_endpoint_attributes( add_ep_output, eventgrid_endpoint, ADTEndpointType.eventgridtopic, - MOCK_ENDPOINT_TAGS, ) servicebus_rg = settings.env.azext_dt_ep_rg @@ -284,14 +282,14 @@ def test_dt_endpoints_routes(self): logger.debug("Adding servicebus topic endpoint...") add_ep_output = self.cmd( - "dt endpoint create servicebus -n {} --sbg {} --sbn {} --sbp {} --sbt {} --en {} --tags {}".format( + "dt endpoint create servicebus -n {} --sbg {} --sbn {} --sbp {} --sbt {} --en {} --dsu {}".format( endpoints_instance_name, servicebus_rg, servicebus_namespace, servicebus_policy, servicebus_topic, servicebus_endpoint, - MOCK_ENDPOINT_TAGS, + MOCK_DEAD_LETTER_SECRET ) ).get_output_in_json() @@ -299,7 +297,6 @@ def test_dt_endpoints_routes(self): add_ep_output, servicebus_endpoint, ADTEndpointType.servicebus, - MOCK_ENDPOINT_TAGS, ) eventhub_rg = settings.env.azext_dt_ep_rg @@ -310,7 +307,7 @@ def test_dt_endpoints_routes(self): logger.debug("Adding eventhub endpoint...") add_ep_output = self.cmd( - "dt endpoint create eventhub -n {} --ehg {} --ehn {} --ehp {} --eh {} --ehs {} --en {}".format( + "dt endpoint create eventhub -n {} --ehg {} --ehn {} --ehp {} --eh {} --ehs {} --en {} --dsu {}".format( endpoints_instance_name, eventhub_rg, eventhub_namespace, @@ -318,11 +315,12 @@ def test_dt_endpoints_routes(self): eventhub_topic, self.current_subscription, eventhub_endpoint, + MOCK_DEAD_LETTER_SECRET ) ).get_output_in_json() assert_common_endpoint_attributes( - add_ep_output, eventhub_endpoint, ADTEndpointType.eventhub, None, + add_ep_output, eventhub_endpoint, ADTEndpointType.eventhub ) show_ep_output = self.cmd( @@ -332,7 +330,7 @@ def test_dt_endpoints_routes(self): ).get_output_in_json() assert_common_endpoint_attributes( - show_ep_output, eventhub_endpoint, ADTEndpointType.eventhub, None, + show_ep_output, eventhub_endpoint, ADTEndpointType.eventhub ) show_ep_output = self.cmd( @@ -345,7 +343,6 @@ def test_dt_endpoints_routes(self): show_ep_output, servicebus_endpoint, ADTEndpointType.servicebus, - MOCK_ENDPOINT_TAGS, ) list_ep_output = self.cmd( @@ -425,17 +422,20 @@ def test_dt_endpoints_routes(self): for endpoint_name in endpoint_names: logger.debug("Cleaning up {} endpoint...".format(endpoint_name)) is_last = endpoint_name == endpoint_names[-1] - delete_ep_output = self.cmd( + self.cmd( "dt endpoint delete -n {} --en {} {}".format( endpoints_instance_name, endpoint_name, "-g {}".format(self.dt_resource_group) if is_last else "", ) - ).get_output_in_json() - assert delete_ep_output["provisioningState"] == "Deleting" - assert delete_ep_output["id"].endswith("/{}".format(endpoint_name)) - sleep(15) # Wait for service to catch-up. Service will fix at some point. + ) + list_endpoint_output = self.cmd( + "dt endpoint list -n {} -g {}".format( + endpoints_instance_name, self.dt_resource_group + ) + ).get_output_in_json() + assert len(list_endpoint_output) == 0 self.cmd( "dt delete -n {} -g {}".format( endpoints_instance_name, self.dt_resource_group @@ -467,7 +467,7 @@ def assert_common_route_attributes( def assert_common_endpoint_attributes( - endpoint_output, endpoint_name, endpoint_type, tags + endpoint_output, endpoint_name, endpoint_type, dead_letter_secret=None ): assert endpoint_output["id"].endswith("/{}".format(endpoint_name)) assert ( @@ -476,14 +476,13 @@ def assert_common_endpoint_attributes( ) assert endpoint_output["resourceGroup"] - if tags: - assert endpoint_output["properties"]["tags"] == validate_key_value_pairs(tags) - assert endpoint_output["properties"]["provisioningState"] assert endpoint_output["properties"]["createdTime"] + if dead_letter_secret: + assert endpoint_output["properties"]["deadLetterSecret"] if endpoint_type == ADTEndpointType.eventgridtopic: - assert endpoint_output["properties"]["TopicEndpoint"] + assert endpoint_output["properties"]["topicEndpoint"] assert endpoint_output["properties"]["accessKey1"] assert endpoint_output["properties"]["accessKey2"] assert endpoint_output["properties"]["endpointType"] == "EventGrid" @@ -494,8 +493,8 @@ def assert_common_endpoint_attributes( assert endpoint_output["properties"]["endpointType"] == "ServiceBus" return if endpoint_type == ADTEndpointType.eventhub: - assert endpoint_output["properties"]["connectionString-PrimaryKey"] - assert endpoint_output["properties"]["connectionString-SecondaryKey"] + assert endpoint_output["properties"]["connectionStringPrimaryKey"] + assert endpoint_output["properties"]["connectionStringSecondaryKey"] assert endpoint_output["properties"]["endpointType"] == "EventHub" return 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 329631f91..c63c89291 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -24,9 +24,9 @@ def __init__(self, test_case): def test_dt_twin(self): instance_name = generate_resource_id() models_directory = "./models" - floor_dtmi = "dtmi:example:Floor;1" + floor_dtmi = "dtmi:com:example:Floor;1" floor_twin_id = "myfloor" - room_dtmi = "dtmi:example:Room;1" + room_dtmi = "dtmi:com:example:Room;1" room_twin_id = "myroom" thermostat_component_id = "Thermostat" @@ -42,7 +42,7 @@ def test_dt_twin(self): ) ) # Wait for RBAC to catch-up - sleep(10) + sleep(15) self.cmd( "dt model create -n {} --from-directory '{}'".format( @@ -67,6 +67,14 @@ def test_dt_twin(self): } ) + self.kwargs["emptyThermostatComponentJson"] = json.dumps( + { + "Thermostat": { + "$metadata": {} + } + } + ) + floor_twin = self.cmd( "dt twin create -n {} --dtmi {} --twin-id {}".format( instance_name, floor_dtmi, floor_twin_id @@ -79,6 +87,38 @@ def test_dt_twin(self): expected_dtmi=floor_dtmi, ) + # twin create with component - example of bare minimum --properties + + # create twin will fail without --properties + self.cmd( + "dt twin create -n {} -g {} --dtmi {} --twin-id {}".format( + instance_name, + self.dt_resource_group, + room_dtmi, + room_twin_id + ), + expect_failure=True + ) + + # minimum component object with empty $metadata object + min_room_twin = self.cmd( + "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( + instance_name, + self.dt_resource_group, + room_dtmi, + room_twin_id, + "{emptyThermostatComponentJson}", + ) + ).get_output_in_json() + + assert_twin_attributes( + twin=min_room_twin, + expected_twin_id=room_twin_id, + expected_dtmi=room_dtmi, + properties=self.kwargs["emptyThermostatComponentJson"], + component_name=thermostat_component_id, + ) + room_twin = self.cmd( "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( instance_name, @@ -332,7 +372,7 @@ def test_dt_twin(self): else "", ) ) - sleep(4) # Wait for API to catch up + sleep(10) # Wait for API to catch up twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins' --cost".format( instance_name, self.dt_resource_group @@ -358,20 +398,11 @@ def assert_twin_attributes( if properties: properties = json.loads(properties) + assert properties for key in properties: if key != component_name: - assert metadata[key]["desiredValue"] == properties[key] - - if component_name: - component_metadata = twin[component_name]["$metadata"] - component_props = properties[component_name] - - for key in component_props: - if key != "$metadata": - assert ( - component_metadata[key]["desiredValue"] == component_props[key] - ) + assert properties[key] == twin[key] def assert_twin_relationship_attributes( diff --git a/setup.py b/setup.py index aac6711e7..26a4472e8 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup( name=PACKAGE_NAME, version=VERSION, - python_requires=">=3.5,<4", + python_requires=">=3.6,<4", description=short_description, long_description="{} Intended for power users and/or automation of IoT solutions at scale.".format(short_description), license="MIT", From d776a571602d30b8fec4cf5d510965db2840ed2b Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Thu, 22 Oct 2020 17:24:46 -0700 Subject: [PATCH 129/179] Prepare release. --- .azure-devops/create-release.yml | 2 +- azext_iot/constants.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index be446801a..d39db27bb 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -46,7 +46,7 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.6.x' - runUnitTestsOnly: 'true' + runUnitTestsOnly: 'false' - script: 'pip install .' displayName: 'Install the whl locally' diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 39fbb19c4..0d4577d68 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.3" +VERSION = "0.10.4" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" From f0475654290a5253c674560b9ea47cbaa8ecdbdd Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 26 Oct 2020 13:29:28 -0700 Subject: [PATCH 130/179] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8286e405..03545931f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Please refer to the official `az iot` reference on [Microsoft Docs](https://docs ## Installation 1. Install the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) - - You must have at least `v2.0.70`, which you can verify with `az --version` + - You must have at least `v2.3.1`, which you can verify with `az --version` 1. Add, Update or Remove the IoT extension with the following commands: - Add: `az extension add --name azure-iot` - Update: `az extension update --name azure-iot` From ac9867d0d4ec3458c1b6e6a882fc939aa2582866 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 26 Oct 2020 13:59:15 -0700 Subject: [PATCH 131/179] Support edgeHub 1.1 routes. (#263) --- azext_iot/_params.py | 6 ++ azext_iot/assets/edge-deploy-2.0.schema.json | 59 ++++++++++-- azext_iot/operations/hub.py | 4 +- .../test_edge_deployment_v11.json | 91 +++++++++++++++++++ .../configurations/test_iot_config_int.py | 32 ++++++- .../configurations/test_iot_config_unit.py | 9 +- 6 files changed, 186 insertions(+), 15 deletions(-) create mode 100644 azext_iot/tests/iothub/configurations/test_edge_deployment_v11.json diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 5cab38d85..5b373b412 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -755,6 +755,12 @@ def load_arguments(self, _): "deployment will merge with routes of the base deployment. Routes with the same name will be " "overwritten based on deployment priority.", ) + context.argument( + "no_validation", + options_list=["--no-validation"], + arg_type=get_three_state_flag(), + help="Disables client side schema validation for edge deployment creation.", + ) with self.argument_context("iot dps") as context: context.argument( diff --git a/azext_iot/assets/edge-deploy-2.0.schema.json b/azext_iot/assets/edge-deploy-2.0.schema.json index 70332856f..fb8794bfc 100644 --- a/azext_iot/assets/edge-deploy-2.0.schema.json +++ b/azext_iot/assets/edge-deploy-2.0.schema.json @@ -33,7 +33,8 @@ "schemaVersion": { "type": "string", "examples": [ - "1.0" + "1.0", + "1.1" ] }, "runtime": { @@ -162,6 +163,9 @@ }, "imagePullPolicy": { "$ref": "#/definitions/imagePullPolicy" + }, + "startupOrder": { + "$ref": "#/definitions/startupOrder" } }, "patternProperties": { @@ -209,6 +213,9 @@ }, "imagePullPolicy": { "$ref": "#/definitions/imagePullPolicy" + }, + "startupOrder": { + "$ref": "#/definitions/startupOrder" } }, "patternProperties": { @@ -249,18 +256,49 @@ "schemaVersion": { "type": "string", "examples": [ - "1.0" + "1.0", + "1.1" ] }, "routes": { "type": "object", "patternProperties": { "^[^\\.\\$# ]+$": { - "type": "string", - "examples": [ - "FROM /* INTO $upstream" - ], - "pattern": "^.+$" + "anyOf": [ + { + "type": "object", + "required": [ + "route" + ], + "properties": { + "route": { + "type": "string", + "examples": [ + "FROM /* INTO $upstream" + ], + "pattern": "^.+$" + }, + "priority": { + "type": "integer", + "minimum": 0, + "maximum": 9 + }, + "timeToLiveSecs": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + } + }, + "additionalProperties": false + }, + { + "type": "string", + "examples": [ + "FROM /* INTO $upstream" + ], + "pattern": "^.+$" + } + ] } }, "additionalProperties": false @@ -341,6 +379,11 @@ "on-create" ] }, + "startupOrder": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + }, "moduleSettings": { "type": "object", "required": [ @@ -387,4 +430,4 @@ "contentMediaType": "application/json" } } -} \ No newline at end of file +} diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index f997bfbea..456ba0337 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -929,10 +929,12 @@ def iot_edge_deployment_create( labels=None, metrics=None, layered=False, + no_validation=False, resource_group_name=None, login=None, ): - config_type = ConfigType.layered if layered else ConfigType.edge + # Short-term fix for --no-validation + config_type = ConfigType.layered if layered or no_validation else ConfigType.edge return _iot_hub_configuration_create( cmd=cmd, config_id=config_id, diff --git a/azext_iot/tests/iothub/configurations/test_edge_deployment_v11.json b/azext_iot/tests/iothub/configurations/test_edge_deployment_v11.json new file mode 100644 index 000000000..8b7df56b4 --- /dev/null +++ b/azext_iot/tests/iothub/configurations/test_edge_deployment_v11.json @@ -0,0 +1,91 @@ +{ + "modulesContent": { + "$edgeAgent": { + "properties.desired": { + "schemaVersion": "1.1", + "runtime": { + "type": "docker", + "settings": { + "minDockerVersion": "v1.25", + "loggingOptions": "", + "registryCredentials": { + "ContosoRegistry": { + "username": "myacr", + "password": "", + "address": "myacr.azurecr.io" + } + } + } + }, + "systemModules": { + "edgeAgent": { + "type": "docker", + "settings": { + "image": "mcr.microsoft.com/azureiotedge-agent:1.0", + "createOptions": "" + } + }, + "edgeHub": { + "type": "docker", + "status": "running", + "restartPolicy": "always", + "startupOrder": 0, + "settings": { + "image": "mcr.microsoft.com/azureiotedge-hub:1.0", + "createOptions": "{\"HostConfig\":{\"PortBindings\":{\"443/tcp\":[{\"HostPort\":\"443\"}],\"5671/tcp\":[{\"HostPort\":\"5671\"}],\"8883/tcp\":[{\"HostPort\":\"8883\"}]}}}" + } + } + }, + "modules": { + "SimulatedTemperatureSensor": { + "version": "1.0", + "type": "docker", + "status": "running", + "restartPolicy": "always", + "startupOrder": 2, + "settings": { + "image": "mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0", + "createOptions": "{}" + } + }, + "filtermodule": { + "version": "1.0", + "type": "docker", + "status": "running", + "restartPolicy": "always", + "startupOrder": 1, + "env": { + "tempLimit": { + "value": "100" + } + }, + "settings": { + "image": "myacr.azurecr.io/filtermodule:latest", + "createOptions": "{}" + } + } + } + } + }, + "$edgeHub": { + "properties.desired": { + "schemaVersion": "1.1", + "routes": { + "sensorToFilter": { + "route": "FROM /messages/modules/SimulatedTemperatureSensor/outputs/temperatureOutput INTO BrokeredEndpoint(\"/modules/filtermodule/inputs/input1\")", + "priority": 0, + "timeToLiveSecs": 1800 + }, + "filterToIoTHub": { + "route": "FROM /messages/modules/filtermodule/outputs/output1 INTO $upstream", + "priority": 1, + "timeToLiveSecs": 1800 + } + }, + "storeAndForwardConfiguration": { + "timeToLiveSecs": 100 + } + } + } + } +} \ No newline at end of file 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 68aba4382..8b6c00429 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_int.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_int.py @@ -20,6 +20,7 @@ edge_content_layered_path = get_context_path( __file__, "test_edge_deployment_layered.json" ) +edge_content_v11_path = get_context_path(__file__, "test_edge_deployment_v11.json") edge_content_v1_path = get_context_path(__file__, "test_edge_deployment_v1.json") edge_content_malformed_path = get_context_path( __file__, "test_edge_deployment_malformed.json" @@ -87,7 +88,7 @@ def __init__(self, test_case): ) def test_edge_deployments(self): - config_count = 4 + config_count = 5 config_ids = self.generate_config_names(config_count) self.kwargs["generic_metrics"] = read_file_content(generic_metrics_path) @@ -250,6 +251,31 @@ def test_edge_deployments(self): expect_failure=True, ) + # 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"], + ), + self.check("metrics.queries", {}), + ], + ) + # Show deployment self.cmd( "iot edge deployment show --deployment-id {} --hub-name {} --resource-group {}".format( @@ -363,7 +389,7 @@ def test_edge_deployments(self): ) config_list_check = [ - self.check("length([*])", 4), + 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])), @@ -655,7 +681,7 @@ def test_device_configurations(self): ) config_list_check = [ - self.check("length([*])", 3), + 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])) diff --git a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py index d72cdaa98..5b185427f 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py @@ -32,11 +32,12 @@ def sample_config_edge_malformed(set_cwd): return result -@pytest.fixture(params=["file", "inlineA", "inlineB", "layered", "v1"]) +@pytest.fixture(params=["file", "inlineA", "inlineB", "layered", "v1", "v11"]) def sample_config_edge(set_cwd, request): path = "test_edge_deployment.json" layered_path = "test_edge_deployment_layered.json" v1_path = "test_edge_deployment_v1.json" + v11_path = "test_edge_deployment_v11.json" payload = None if request.param == "inlineA": @@ -49,6 +50,8 @@ def sample_config_edge(set_cwd, request): payload = json.dumps(json.loads(read_file_content(layered_path))) elif request.param == "v1": payload = json.dumps(json.loads(read_file_content(v1_path))) + elif request.param == "v11": + payload = json.dumps(json.loads(read_file_content(v11_path))) return (request.param, payload) @@ -289,7 +292,7 @@ def test_config_create_edge( assert body.get("priority") == priority assert body.get("labels") == evaluate_literal(labels, dict) - if sample_config_edge[0] == "inlineB": + if sample_config_edge[0] == "inlineB" or sample_config_edge[0] == "v11": assert ( body["content"]["modulesContent"] == json.loads(sample_config_edge[1])["modulesContent"] @@ -792,7 +795,7 @@ def test_config_apply_edge( in url ) - if sample_config_edge[0] == "inlineB": + if sample_config_edge[0] == "inlineB" or sample_config_edge[0] == "v11": assert ( body["modulesContent"] == json.loads(sample_config_edge[1])["modulesContent"] From 3fe8ecb676b44716d945f04efbbdcfe579ba7532 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Mon, 26 Oct 2020 14:18:35 -0700 Subject: [PATCH 132/179] Minor DT GA updates (#264) * Updated help for digitaltwins GA RBAC roles * Updated DT tests to use new roles * Skipping AICS tests due to environment inconsistencies for now * Increased DT create default polling timeout to 60 seconds --- azext_iot/digitaltwins/_help.py | 6 +++--- azext_iot/digitaltwins/providers/resource.py | 2 +- azext_iot/tests/digitaltwins/__init__.py | 4 ++-- azext_iot/tests/product/__init__.py | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index f66740bbc..6f6b48831 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -184,11 +184,11 @@ def load_digitaltwins_help(): examples: - name: Assign a user (by email) the built-in Digital Twins Owner role against a target instance. text: > - az dt role-assignment create -n {instance_name} --assignee "owneruser@microsoft.com" --role "Azure Digital Twins Owner (Preview)" + az dt role-assignment create -n {instance_name} --assignee "owneruser@microsoft.com" --role "Azure Digital Twins Data Owner" - name: Assign a user (by object Id) the built-in Digital Twins Reader role against a target instance. text: > - az dt role-assignment create -n {instance_name} --assignee "97a89267-0966-4054-a156-b7d86ef8e216" --role "Azure Digital Twins Reader (Preview)" + az dt role-assignment create -n {instance_name} --assignee "97a89267-0966-4054-a156-b7d86ef8e216" --role "Azure Digital Twins Data Reader" - name: Assign a service principal a custom role against a target instance. text: > @@ -205,7 +205,7 @@ def load_digitaltwins_help(): examples: - name: Remove a user from a specific role assignment of a Digital Twins instance. text: > - az dt role-assignment delete -n {instance_name} --assignee "removeuser@microsoft.com" --role "Azure Digital Twins Reader (Preview)" + az dt role-assignment delete -n {instance_name} --assignee "removeuser@microsoft.com" --role "Azure Digital Twins Data Reader" - name: Remove a user from all assigned roles of a Digital Twins instance. text: > diff --git a/azext_iot/digitaltwins/providers/resource.py b/azext_iot/digitaltwins/providers/resource.py index dfebc3617..5f8dc34f9 100644 --- a/azext_iot/digitaltwins/providers/resource.py +++ b/azext_iot/digitaltwins/providers/resource.py @@ -25,7 +25,7 @@ def __init__(self, cmd): self.mgmt_sdk = self.get_mgmt_sdk() self.rbac = RbacProvider() - def create(self, name, resource_group_name, location=None, tags=None, timeout=30): + def create(self, name, resource_group_name, location=None, tags=None, timeout=60): if tags: tags = validate_key_value_pairs(tags) diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py index d11377b2b..be44dc153 100644 --- a/azext_iot/tests/digitaltwins/__init__.py +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -23,8 +23,8 @@ def generate_resource_id(): class DTLiveScenarioTest(LiveScenarioTest): role_map = { - "owner": "Azure Digital Twins Owner (Preview)", - "reader": "Azure Digital Twins Reader (Preview)", + "owner": "Azure Digital Twins Data Owner", + "reader": "Azure Digital Twins Data Reader", } def __init__(self, test_scenario): diff --git a/azext_iot/tests/product/__init__.py b/azext_iot/tests/product/__init__.py index abb53840c..757a425b4 100644 --- a/azext_iot/tests/product/__init__.py +++ b/azext_iot/tests/product/__init__.py @@ -5,8 +5,10 @@ # -------------------------------------------------------------------------------------------- from azure.cli.testsdk import LiveScenarioTest +import pytest +@pytest.mark.skipif(True, reason="Skipping AICS tests due to environment inconsistencies") class AICSLiveScenarioTest(LiveScenarioTest): def __init__(self, test_scenario): assert test_scenario From b790dc2f829fc6b8feb4778685910d2442b2ba6f Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 26 Oct 2020 14:22:34 -0700 Subject: [PATCH 133/179] Update HISTORY.rst --- HISTORY.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index cb899029b..7ec827a5d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,23 @@ Release History =============== +0.10.4 ++++++++++++++++ + +**General updates** + +* IoT extension installation constrained to Python 3.6 or greater. + +**Azure Digital Twins updates** + +* ADT GA updates and release. + +**IoT Edge** + +* Validation schema updated with $edgeHub 1.1 route option. +* Introduces `--no-validation` to skip client side schema based validation for edge deployments. + + 0.10.3 +++++++++++++++ From 3f764629ff8be90d4e008c23228cb9f73a9cc855 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 26 Oct 2020 14:23:17 -0700 Subject: [PATCH 134/179] Update HISTORY.rst --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7ec827a5d..88257f4a1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,7 +17,7 @@ Release History **IoT Edge** * Validation schema updated with $edgeHub 1.1 route option. -* Introduces `--no-validation` to skip client side schema based validation for edge deployments. +* Introduces `--no-validation` to skip client side schema based validation for edge deployment creation. 0.10.3 From 21b8814987a215acd356486612d7c71bbaadc9a6 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Mon, 26 Oct 2020 17:31:05 -0700 Subject: [PATCH 135/179] Fallback on release pipeline. --- .azure-devops/create-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index d39db27bb..be446801a 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -46,7 +46,7 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.6.x' - runUnitTestsOnly: 'false' + runUnitTestsOnly: 'true' - script: 'pip install .' displayName: 'Install the whl locally' From c010a3af3a3074a536dbf86dc5189817a35761ee Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 27 Oct 2020 12:02:32 -0700 Subject: [PATCH 136/179] Constrain Python versions. --- .azure-devops/merge.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index dd16a2e9e..327681135 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -67,7 +67,7 @@ jobs: steps: - template: templates/run-tests.yml parameters: - pythonVersion: '3.x' + pythonVersion: '3.8.x' - job: 'run_unit_tests_windows' dependsOn : [ 'build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] @@ -82,7 +82,7 @@ jobs: - template: templates/run-tests.yml parameters: - pythonVersion: '3.x' + pythonVersion: '3.8.x' - job: 'run_style_check' dependsOn: ['build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] @@ -92,7 +92,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.x' + versionSpec: '3.8.x' architecture: 'x64' - template: templates/install-azure-cli-released.yml @@ -117,7 +117,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.6' + versionSpec: '3.6.x' architecture: 'x64' - template: templates/install-configure-azure-cli.yml From 3e28cd6f7ef42e6a0e28d42b92aa5c5b45d4fa1e Mon Sep 17 00:00:00 2001 From: Ryan K Date: Thu, 29 Oct 2020 17:54:18 -0700 Subject: [PATCH 137/179] Switched DT instance create --tags parameter to common CLI tags_type property (#266) --- azext_iot/digitaltwins/_help.py | 2 +- azext_iot/digitaltwins/params.py | 4 +++- azext_iot/digitaltwins/providers/resource.py | 4 +--- azext_iot/tests/digitaltwins/__init__.py | 3 +-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index 6f6b48831..67fc1a77a 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -28,7 +28,7 @@ def load_digitaltwins_help(): - name: Create instance in target resource group with specified location and tags. text: > - az dt create -n {instance_name} -g {resouce_group} -l westcentralus --tags "a=b;c=d" + az dt create -n {instance_name} -g {resouce_group} -l westcentralus --tags a=b c=d """ helps["dt show"] = """ diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index 6c28305bb..90a1edfc4 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -12,6 +12,7 @@ from azure.cli.core.commands.parameters import ( resource_group_name_type, get_three_state_flag, + tags_type ) depfor_type = CLIArgumentType( @@ -55,7 +56,8 @@ def load_digitaltwins_arguments(self, _): context.argument( "tags", options_list=["--tags"], - help="Digital Twins instance tags. Property bag in key-value pairs with the following format: a=b;c=d.", + arg_type=tags_type, + help="Digital Twins instance tags. Property bag in key-value pairs with the following format: a=b c=d", ) context.argument( "endpoint_name", diff --git a/azext_iot/digitaltwins/providers/resource.py b/azext_iot/digitaltwins/providers/resource.py index 5f8dc34f9..97db61f7b 100644 --- a/azext_iot/digitaltwins/providers/resource.py +++ b/azext_iot/digitaltwins/providers/resource.py @@ -15,7 +15,7 @@ EventHub as EventHubEndpointProperties, ServiceBus as ServiceBusEndpointProperties, ) -from azext_iot.common.utility import validate_key_value_pairs, unpack_msrest_error +from azext_iot.common.utility import unpack_msrest_error from knack.util import CLIError @@ -26,8 +26,6 @@ def __init__(self, cmd): self.rbac = RbacProvider() def create(self, name, resource_group_name, location=None, tags=None, timeout=60): - if tags: - tags = validate_key_value_pairs(tags) if not location: from azext_iot.common.embedded_cli import EmbeddedCLI diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py index be44dc153..20f06e545 100644 --- a/azext_iot/tests/digitaltwins/__init__.py +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -12,8 +12,7 @@ from azext_iot.common.embedded_cli import EmbeddedCLI -MOCK_RESOURCE_TAGS = "a=b;c=d" -MOCK_ENDPOINT_TAGS = "key0=value0;key1=value1;" +MOCK_RESOURCE_TAGS = "a=b c=d" MOCK_DEAD_LETTER_SECRET = 'https://accountname.blob.core.windows.net/containerName?sasToken' From 420461d7920ed541092c2bf1632a3cef68e6fbbe Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Thu, 29 Oct 2020 18:28:23 -0700 Subject: [PATCH 138/179] Prepare v0.10.5 --- HISTORY.rst | 9 +++++++++ azext_iot/constants.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 88257f4a1..869ef3d67 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,15 @@ Release History =============== +0.10.5 ++++++++++++++++ + +**Azure Digital Twins updates** + +* Breaking change on the `--tags` parameter for `az dt create`. The prior input format of --tags "a=b;c=d" has been + changed to --tags a=b c=d to be more consistent with other Az CLI tag formats. + + 0.10.4 +++++++++++++++ diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 0d4577d68..b02b5052e 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.4" +VERSION = "0.10.5" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" From a0b4ecd9c1c59d7ca54d9983f3a2efb63f5bde3e Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Fri, 30 Oct 2020 15:47:20 -0700 Subject: [PATCH 139/179] Remove Py3.5 metadata classifier. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 26a4472e8..501122e41 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,6 @@ "Intended Audience :: System Administrators", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From 1854746d03783bed212019c12cee92f299df09f1 Mon Sep 17 00:00:00 2001 From: Paul Montgomery Date: Tue, 3 Nov 2020 16:44:34 -0800 Subject: [PATCH 140/179] AICS fix (#268) * Fix parameter name * Update unit tests * Update to history --- HISTORY.rst | 63 ++++++++++--------- azext_iot/product/providers/aics.py | 2 +- .../product/test_command_test_create_unit.py | 10 +-- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 869ef3d67..424409808 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,13 +3,20 @@ Release History =============== +0.10.6 ++++++++++++++++ + +**Azure IoT Product Certification service** + +* Fix bug for `az iot product test create` not specifying query parameter "GenerateProvisioningConfiguration" appropriately. + 0.10.5 +++++++++++++++ **Azure Digital Twins updates** * Breaking change on the `--tags` parameter for `az dt create`. The prior input format of --tags "a=b;c=d" has been - changed to --tags a=b c=d to be more consistent with other Az CLI tag formats. + changed to --tags a=b c=d to be more consistent with other Az CLI tag formats. 0.10.4 @@ -35,7 +42,7 @@ Release History **General updates** * Python 3.5 support will soon be dropped corresponding with the official end of life date. -* Formal python requires constraint added to constrain installs to Py 3.5+. +* Formal python requires constraint added to constrain installs to Py 3.5+. **IoT Plug-and-Play updates** @@ -78,7 +85,7 @@ Release History **Breaking Changes** -* `az iot dps enrollment show` and `az iot dps enrollment-group show` now return raw service results instead of deserialized models. +* `az iot dps enrollment show` and `az iot dps enrollment-group show` now return raw service results instead of deserialized models. This means that some properties that were previously returned as `null` for these commands will no longer be returned, possibly causing a breaking change. @@ -95,9 +102,9 @@ Release History * Introduces 'az iot central device compute-device-key' preview command to generate derived device SAS key * This release involves a re-grouping of IoT Central commands. - + Set of changes for GA commands - + * 'az iot central app device-twin' is deprecated use 'az iot central device twin' instead. Deprecated command group is planned to be removed by December 2020 * 'az iot central app monitor-events' is deprecated use 'az iot central diagnostics monitor-events' instead. Deprecated command is planned to be removed by December 2020 @@ -154,7 +161,7 @@ Introducing preview commands for the Azure IoT Product Certification service * Use 'az iot product requirement' to manage product certification requirements * Use 'az iot product test' to manage device tests for certification - + * The product test command group encompasses test cases, runs and tasks IoT Central updates @@ -464,27 +471,27 @@ Known issues * Significant restructing of CLI, prioritizes pure Python solutions where possible * Provides IoT Edge capabilities * Adds following new commands: -* iot query -* iot device show -* iot device list -* iot device create -* iot device update -* iot device delete -* iot device twin show -* iot device twin update -* iot device module show -* iot device module list -* iot device module create -* iot device module update -* iot device module delete -* iot device module twin show -* iot device module twin update -* iot device module twin replace -* iot configuration apply -* iot configuration create -* iot configuration update -* iot configuration delete -* iot configuration show +* iot query +* iot device show +* iot device list +* iot device create +* iot device update +* iot device delete +* iot device twin show +* iot device twin update +* iot device module show +* iot device module list +* iot device module create +* iot device module update +* iot device module delete +* iot device module twin show +* iot device module twin update +* iot device module twin replace +* iot configuration apply +* iot configuration create +* iot configuration update +* iot configuration delete +* iot configuration show * iot configuration list * Bug fixes @@ -499,6 +506,6 @@ Known issues * Show and update device twin * Invoke device method * Device simulation -* Hub message send (Cloud-to-device) +* Hub message send (Cloud-to-device) * New device message send (Device-to-cloud) supports http, amqp, mqtt * Get SAS token diff --git a/azext_iot/product/providers/aics.py b/azext_iot/product/providers/aics.py index ef49d5e1d..45108c6f9 100644 --- a/azext_iot/product/providers/aics.py +++ b/azext_iot/product/providers/aics.py @@ -94,7 +94,7 @@ def create_test( self, test_configuration, provisioning=True, ): return self.mgmt_sdk.create_device_test( - provisioning=provisioning, body=test_configuration + generate_provisioning_configuration=provisioning, body=test_configuration ) # Test runs diff --git a/azext_iot/tests/product/test_command_test_create_unit.py b/azext_iot/tests/product/test_command_test_create_unit.py index d4a557961..b749b5218 100644 --- a/azext_iot/tests/product/test_command_test_create_unit.py +++ b/azext_iot/tests/product/test_command_test_create_unit.py @@ -124,7 +124,7 @@ def test_create_with_default_badge_type_doesnt_check_models( mock_process_models.assert_not_called() mock_service.assert_called_with( - provisioning=True, + generate_provisioning_configuration=True, body={ "validationType": "Test", "productId": None, @@ -157,7 +157,7 @@ def test_create_with_pnp_badge_type_checks_models( mock_process_models.assert_called_with("models_folder") mock_service.assert_called_with( - provisioning=True, + generate_provisioning_configuration=True, body={ "validationType": "Test", "productId": None, @@ -205,7 +205,7 @@ def test_create_with_cert_auth_reads_cert_file( mock_read_certificate.assert_called_with("mycertificate.cer") mock_process_models.assert_called_with("models_folder") mock_service.assert_called_with( - provisioning=True, + generate_provisioning_configuration=True, body={ "validationType": "Certification", "productId": "ABC123", @@ -253,7 +253,7 @@ def test_create_with_tpm( mock_read_certificate.assert_not_called() mock_process_models.assert_called_with("models_folder") mock_service.assert_called_with( - provisioning=True, + generate_provisioning_configuration=True, body={ "validationType": "Test", "productId": None, @@ -282,7 +282,7 @@ def test_create_with_configuration_file(self, mock_from_file, mock_sdk_create): mock_from_file.return_value = mock_file_data create(self, configuration_file="somefile") mock_from_file.assert_called_with("somefile") - mock_sdk_create.assert_called_with(provisioning=True, body=mock_file_data) + mock_sdk_create.assert_called_with(generate_provisioning_configuration=True, body=mock_file_data) @mock.patch("os.scandir") @mock.patch("os.path.isfile") From 55c192d0954abe887e979d0f14bd160a80fe2502 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Wed, 4 Nov 2020 14:26:54 -0800 Subject: [PATCH 141/179] Test and discovery updates (#267) * Updated tests to correctly check DT instance tags * Added 'active' filter to hubs in connection_string_show to avoid error fetching keys from inactive hubs * Added IoT Hub State type enumeration --- azext_iot/common/shared.py | 26 +++++++++++++++++++ azext_iot/operations/hub.py | 5 ++-- azext_iot/tests/digitaltwins/__init__.py | 9 +++++-- .../test_dt_resource_lifecycle_int.py | 8 +++--- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index 484a3f224..6440659cd 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -220,6 +220,32 @@ class RegenerateKeyType(Enum): """ Target key type for regeneration. """ + primary = KeyType.primary.value secondary = KeyType.secondary.value swap = "swap" + + +class IoTHubStateType(Enum): + """ + IoT Hub State Property + """ + + Activating = "Activating" + Active = "Active" + Deleting = "Deleting" + Deleted = "Deleted" + ActivationFailed = "ActivationFailed" + DeletionFailed = "DeletionFailed" + Transitioning = "Transitioning" + Suspending = "Suspending" + Suspended = "Suspended" + Resuming = "Resuming" + FailingOver = "FailingOver" + FailoverFailed = "FailoverFailed" + TenantCommitted = "TenantCommitted" + Restoring = "Restoring" + IdentityCreated = "IdentityCreated" + KeyEncryptionKeyRevoking = "KeyEncryptionKeyRevoking" + KeyEncryptionKeyRevoked = "KeyEncryptionKeyRevoked" + ReActivating = "ReActivating" diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 456ba0337..47ccb9950 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -24,7 +24,8 @@ ConfigType, KeyType, SettleType, - RegenerateKeyType + RegenerateKeyType, + IoTHubStateType ) from azext_iot.iothub.providers.discovery import IotHubDiscovery from azext_iot.common.utility import ( @@ -2609,7 +2610,7 @@ def conn_str_getter(hub): if show_all else conn_str_getter(hub)[0], } - for hub in hubs + for hub in hubs if hub.properties.state == IoTHubStateType.Active.value ] hub = discovery.find_iothub(hub_name, resource_group_name) if hub: diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py index 20f06e545..5c97dff96 100644 --- a/azext_iot/tests/digitaltwins/__init__.py +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -13,7 +13,10 @@ MOCK_RESOURCE_TAGS = "a=b c=d" -MOCK_DEAD_LETTER_SECRET = 'https://accountname.blob.core.windows.net/containerName?sasToken' +MOCK_RESOURCE_TAGS_DICT = {"a": "b", "c": "d"} +MOCK_DEAD_LETTER_SECRET = ( + "https://accountname.blob.core.windows.net/containerName?sasToken" +) def generate_resource_id(): @@ -64,7 +67,9 @@ def _init_basic_env_vars(self): "Digital Twins CLI tests requires at least 'azext_iot_testrg' for resource deployment." ) - self._rg_loc = self.embedded_cli.invoke("group show --name {}".format(self._rg)).as_json()["location"] + self._rg_loc = self.embedded_cli.invoke( + "group show --name {}".format(self._rg) + ).as_json()["location"] @property def current_user(self): 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 373bb5f01..4c834cc86 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -7,12 +7,12 @@ import pytest from time import sleep from knack.log import get_logger -from azext_iot.common.utility import validate_key_value_pairs from azext_iot.digitaltwins.common import ADTEndpointType from ..settings import DynamoSettings from . import DTLiveScenarioTest from . import ( MOCK_RESOURCE_TAGS, + MOCK_RESOURCE_TAGS_DICT, MOCK_DEAD_LETTER_SECRET, generate_resource_id, ) @@ -72,7 +72,7 @@ def test_dt_resource(self): instance_names[0], self.dt_resource_group, self.dt_location, - MOCK_RESOURCE_TAGS, + MOCK_RESOURCE_TAGS_DICT, ) # Explictly assert create prevents provisioning on a name conflict (across regions) @@ -110,7 +110,7 @@ def test_dt_resource(self): instance_names[0], self.dt_resource_group, self.dt_location, - MOCK_RESOURCE_TAGS, + MOCK_RESOURCE_TAGS_DICT, ) show_output = self.cmd( @@ -455,7 +455,7 @@ def assert_common_resource_attributes( assert instance_output["provisioningState"] == "Succeeded" assert instance_output["resourceGroup"] == group_id assert instance_output["type"] == "Microsoft.DigitalTwins/digitalTwinsInstances" - assert instance_output["tags"] == validate_key_value_pairs(tags) + assert instance_output["tags"] == tags def assert_common_route_attributes( From e29b41bfa1b6a2eaa3b2f1d87aa4f67ad5a6a63d Mon Sep 17 00:00:00 2001 From: YingXue Date: Mon, 9 Nov 2020 17:05:25 -0800 Subject: [PATCH 142/179] Nested edge (#265) * update sdk * implement nested edge and update tests * update history and integration tests * merge upstream * fix linter and update job operations; address comments * update deprecation message * update nargs and output format of children commands * update history and integration tests * update help test for force option --- HISTORY.rst | 16 +- azext_iot/_help.py | 117 ++- azext_iot/_params.py | 70 +- .../central/providers/devicetwin_provider.py | 2 +- azext_iot/commands.py | 53 +- azext_iot/constants.py | 2 +- azext_iot/iothub/providers/job.py | 14 +- azext_iot/operations/hub.py | 210 +++-- .../service/iot_hub_gateway_service_ap_is.py | 56 +- .../sdk/iothub/service/models/__init__.py | 36 - .../models/authentication_mechanism.py | 11 +- .../models/authentication_mechanism_py3.py | 11 +- .../models/bulk_registry_operation_result.py | 10 +- .../bulk_registry_operation_result_py3.py | 10 +- .../service/models/cloud_to_device_method.py | 7 +- .../models/cloud_to_device_method_py3.py | 7 +- .../iothub/service/models/configuration.py | 29 +- .../service/models/configuration_content.py | 8 +- .../models/configuration_content_py3.py | 8 +- .../service/models/configuration_metrics.py | 6 +- .../models/configuration_metrics_py3.py | 6 +- .../service/models/configuration_py3.py | 29 +- .../configuration_queries_test_input.py | 6 +- .../configuration_queries_test_input_py3.py | 6 +- .../configuration_queries_test_response.py | 6 +- ...configuration_queries_test_response_py3.py | 6 +- .../sdk/iothub/service/models/desired.py | 29 - .../sdk/iothub/service/models/desired_py3.py | 29 - .../iothub/service/models/desired_state.py | 36 - .../service/models/desired_state_py3.py | 36 - azext_iot/sdk/iothub/service/models/device.py | 46 +- .../service/models/device_capabilities.py | 5 +- .../service/models/device_capabilities_py3.py | 5 +- .../service/models/device_job_statistics.py | 12 +- .../models/device_job_statistics_py3.py | 12 +- .../sdk/iothub/service/models/device_py3.py | 48 +- .../models/device_registry_operation_error.py | 30 +- .../device_registry_operation_error_py3.py | 30 +- .../device_registry_operation_warning.py | 8 +- .../device_registry_operation_warning_py3.py | 8 +- .../service/models/digital_twin_interfaces.py | 32 - .../models/digital_twin_interfaces_patch.py | 29 - ..._twin_interfaces_patch_interfaces_value.py | 29 - ...patch_interfaces_value_properties_value.py | 29 - ...terfaces_value_properties_value_desired.py | 29 - ...aces_value_properties_value_desired_py3.py | 29 - ...h_interfaces_value_properties_value_py3.py | 29 - ...n_interfaces_patch_interfaces_value_py3.py | 29 - .../digital_twin_interfaces_patch_py3.py | 29 - .../models/digital_twin_interfaces_py3.py | 32 - .../service/models/export_import_device.py | 50 +- .../models/export_import_device_py3.py | 52 +- .../fault_injection_connection_properties.py | 32 - ...ult_injection_connection_properties_py3.py | 32 - .../models/fault_injection_properties.py | 36 - .../models/fault_injection_properties_py3.py | 36 - .../sdk/iothub/service/models/interface.py | 32 - .../iothub/service/models/interface_py3.py | 32 - .../iothub/service/models/job_properties.py | 63 +- .../service/models/job_properties_py3.py | 65 +- .../sdk/iothub/service/models/job_request.py | 24 +- .../iothub/service/models/job_request_py3.py | 24 +- .../sdk/iothub/service/models/job_response.py | 38 +- .../iothub/service/models/job_response_py3.py | 38 +- azext_iot/sdk/iothub/service/models/module.py | 32 +- .../sdk/iothub/service/models/module_py3.py | 32 +- .../sdk/iothub/service/models/property.py | 32 - .../service/models/property_container.py | 22 +- .../service/models/property_container_py3.py | 22 +- .../sdk/iothub/service/models/property_py3.py | 32 - .../models/purge_message_queue_result.py | 6 +- .../models/purge_message_queue_result_py3.py | 6 +- .../sdk/iothub/service/models/query_result.py | 2 +- .../iothub/service/models/query_result_py3.py | 2 +- .../service/models/query_specification.py | 4 +- .../service/models/query_specification_py3.py | 4 +- .../service/models/registry_statistics.py | 7 +- .../service/models/registry_statistics_py3.py | 7 +- .../sdk/iothub/service/models/reported.py | 32 - .../sdk/iothub/service/models/reported_py3.py | 32 - .../service/models/service_statistics.py | 2 +- .../service/models/service_statistics_py3.py | 2 +- .../iothub/service/models/symmetric_key.py | 4 +- .../service/models/symmetric_key_py3.py | 4 +- azext_iot/sdk/iothub/service/models/twin.py | 75 +- .../iothub/service/models/twin_properties.py | 22 +- .../service/models/twin_properties_py3.py | 22 +- .../sdk/iothub/service/models/twin_py3.py | 77 +- .../iothub/service/models/x509_thumbprint.py | 5 +- .../service/models/x509_thumbprint_py3.py | 5 +- .../sdk/iothub/service/operations/__init__.py | 24 +- .../operations/bulk_registry_operations.py | 104 +++ .../cloud_to_device_messages_operations.py | 249 +++++ .../operations/configuration_operations.py | 84 +- .../operations/device_method_operations.py | 177 ---- ...in_operations.py => devices_operations.py} | 309 ++++--- .../operations/digital_twin_operations.py | 230 ++--- ...lient_operations.py => jobs_operations.py} | 96 +- .../service/operations/modules_operations.py | 574 ++++++++++++ .../service/operations/query_operations.py | 106 +++ .../operations/registry_manager_operations.py | 857 ------------------ ...operations.py => statistics_operations.py} | 57 +- azext_iot/sdk/iothub/service/version.py | 2 +- .../iothub/jobs/test_iothub_jobs_unit.py | 3 +- .../iothub/test_iothub_nested_edge_int.py | 243 +++++ azext_iot/tests/test_iot_ext_int.py | 6 +- azext_iot/tests/test_iot_ext_unit.py | 229 +++-- 107 files changed, 2823 insertions(+), 2945 deletions(-) delete mode 100644 azext_iot/sdk/iothub/service/models/desired.py delete mode 100644 azext_iot/sdk/iothub/service/models/desired_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/desired_state.py delete mode 100644 azext_iot/sdk/iothub/service/models/desired_state_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/digital_twin_interfaces_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/fault_injection_connection_properties.py delete mode 100644 azext_iot/sdk/iothub/service/models/fault_injection_connection_properties_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/fault_injection_properties.py delete mode 100644 azext_iot/sdk/iothub/service/models/fault_injection_properties_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/interface.py delete mode 100644 azext_iot/sdk/iothub/service/models/interface_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/property.py delete mode 100644 azext_iot/sdk/iothub/service/models/property_py3.py delete mode 100644 azext_iot/sdk/iothub/service/models/reported.py delete mode 100644 azext_iot/sdk/iothub/service/models/reported_py3.py create mode 100644 azext_iot/sdk/iothub/service/operations/bulk_registry_operations.py create mode 100644 azext_iot/sdk/iothub/service/operations/cloud_to_device_messages_operations.py delete mode 100644 azext_iot/sdk/iothub/service/operations/device_method_operations.py rename azext_iot/sdk/iothub/service/operations/{twin_operations.py => devices_operations.py} (60%) rename azext_iot/sdk/iothub/service/operations/{job_client_operations.py => jobs_operations.py} (89%) create mode 100644 azext_iot/sdk/iothub/service/operations/modules_operations.py create mode 100644 azext_iot/sdk/iothub/service/operations/query_operations.py delete mode 100644 azext_iot/sdk/iothub/service/operations/registry_manager_operations.py rename azext_iot/sdk/iothub/service/operations/{fault_injection_operations.py => statistics_operations.py} (73%) create mode 100644 azext_iot/tests/iothub/test_iothub_nested_edge_int.py diff --git a/HISTORY.rst b/HISTORY.rst index 424409808..a89bc382c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,6 @@ Release History =============== - 0.10.6 +++++++++++++++ @@ -10,6 +9,21 @@ Release History * Fix bug for `az iot product test create` not specifying query parameter "GenerateProvisioningConfiguration" appropriately. + +**IoT Hub updates** + +* SDK refresh. IoT Hub service calls point to api-version 2020-09-30. + +* Updated nested edge (edge offline) commands to support parentScopes. + + Set of changes + + * 'az iot hub device-identity get-parent' is deprecated use 'az iot hub device-identity parent show' instead. Deprecated command group is planned to be removed by December 2021 + * 'az iot hub device-identity set-parent' is deprecated use 'az iot hub device-identity parent set' instead. Deprecated command is planned to be removed by December 2021 + * 'az iot hub device-identity add-children' is deprecated use 'az iot hub device-identity children add' instead. Deprecated command group is planned to be removed by December 2021 + * 'az iot hub device-identity remove-children' is deprecated use 'az iot hub device-identity children remove' instead. Deprecated command is planned to be removed by December 2021 + * 'az iot hub device-identity list-children' is deprecated use 'az iot hub device-identity children list' instead. Deprecated command group is planned to be removed by December 2021 + 0.10.5 +++++++++++++++ diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 5cdc6e6a8..ec7d000e8 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -298,38 +298,70 @@ examples: - name: Get the parent device of the specified device. text: > - az iot hub device-identity get-parent -d {non_edge_device_id} -n {iothub_name} + 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 non-edge device. + short-summary: Set the parent device of the specified device. examples: - - name: Set the parent device of the specified non-edge device. + - name: Set the parent device of the specified device. text: > - az iot hub device-identity set-parent -d {non_edge_device_id} --pd {edge_device_id} -n {iothub_name} - - name: Set the parent device of the specified non-edge device irrespectively the non-edge device is + 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. text: > - az iot hub device-identity set-parent -d {non_edge_device_id} --pd {edge_device_id} --force -n {iothub_name} + az iot hub device-identity set-parent -d {device_id} --pd {edge_device_id} --force -n {iothub_name} +""" + +helps[ + "iot hub device-identity parent" +] = """ + type: group + short-summary: Manage IoT device\'s parent device. +""" + +helps[ + "iot hub device-identity parent show" +] = """ + 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 parent show -d {device_id} -n {iothub_name} +""" + +helps[ + "iot hub device-identity parent set" +] = """ + type: command + short-summary: Set the parent device of the specified device. + examples: + - name: Set the parent device of the specified device. + text: > + az iot hub device-identity parent set -d {device_id} --pd {edge_device_id} -n {iothub_name} + - name: Set the parent device of the specified device and overwrites its original parent. + text: > + 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 non edge device ids as children of specified edge device. + short-summary: Add specified comma-separated list of device ids as children of specified edge device. examples: - - name: Add non-edge devices as a children to the edge device. + - 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_non_edge_device_id} + az iot hub device-identity add-children -d {edge_device_id} --child-list {comma_separated_device_id} -n {iothub_name} - - name: Add non-edge devices as a children to the edge device irrespectively the non-edge device is + - 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_non_edge_device_id} + az iot hub device-identity add-children -d {edge_device_id} --child-list {comma_separated_device_id} -n {iothub_name} -f """ @@ -337,9 +369,9 @@ "iot hub device-identity list-children" ] = """ type: command - short-summary: Print comma-separated list of assigned child devices. + short-summary: Outputs comma-separated list of assigned child devices. examples: - - name: Show all assigned non-edge devices as comma-separated list. + - name: Show all assigned devices as comma-separated list. text: > az iot hub device-identity list-children -d {edge_device_id} -n {iothub_name} """ @@ -348,17 +380,70 @@ "iot hub device-identity remove-children" ] = """ type: command - short-summary: Remove non edge devices as children from specified edge device. + 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_non_edge_device_id} + az iot hub device-identity remove-children -d {edge_device_id} --child-list {comma_separated_device_id} -n {iothub_name} - - name: Remove all non-edge devices as children specified edge device. + - 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" +] = """ + type: group + short-summary: Manage IoT device\'s children device. +""" + +helps[ + "iot hub device-identity children add" +] = """ + type: command + short-summary: Add specified space-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 children add -d {edge_device_id} --child-list {space_separated_device_id} + -n {iothub_name} + - name: Add devices as children to the edge device and overwrites children devices' + original parent. + text: > + az iot hub device-identity children add -d {edge_device_id} --child-list {space_separated_device_id} + -n {iothub_name} -f +""" + +helps[ + "iot hub device-identity children list" +] = """ + type: command + short-summary: Outputs list of assigned child devices. + examples: + - name: Show all assigned children devices as list. + text: > + az iot hub device-identity children list -d {edge_device_id} -n {iothub_name} + - name: Show all assigned children devices as list whose device ID contains substring of 'test'. + text: > + az iot hub device-identity children list -d {edge_device_id} -n {iothub_name} --query "[?contains(@,'test')]" +""" + +helps[ + "iot hub device-identity children remove" +] = """ + 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 children remove -d {edge_device_id} --child-list {space_separated_device_id} + -n {iothub_name} + - name: Remove all devices as children specified edge device. + text: > + az iot hub device-identity children remove -d {edge_device_id} --remove-all +""" + helps[ "iot hub device-twin" ] = """ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 5b373b412..8bb253ed7 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -49,6 +49,12 @@ "sys = system properties, app = application properties, anno = annotations", ) +children_list_prop_type = CLIArgumentType( + options_list=["--child-list", "--cl"], + nargs="*", + help="Child device list (space separated).", +) + # There is a bug in CLI core preventing treating --qos as an integer. # Until its resolved, ensure casting of value to integer # TODO: azure.cli.core.parser line 180 difflib.get_close_matches @@ -387,17 +393,27 @@ def load_arguments(self, _): context.argument( "force", options_list=["--force", "-f"], - help="Overwrites the non-edge device's parent device.", + 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.", + 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) includes only non-edge devices.", + 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 regenerate-key') as context: @@ -458,10 +474,10 @@ def load_arguments(self, _): ) with self.argument_context("iot hub device-identity get-parent") as context: - context.argument("device_id", help="Id of non-edge device.") + 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 non-edge device.") + context.argument("device_id", help="Id of device.") context.argument( "parent_id", options_list=["--parent-device-id", "--pd"], @@ -470,7 +486,7 @@ def load_arguments(self, _): context.argument( "force", options_list=["--force", "-f"], - help="Overwrites the non-edge device's parent device.", + help="Overwrites the device's parent device.", ) with self.argument_context("iot hub device-identity add-children") as context: @@ -478,12 +494,12 @@ def load_arguments(self, _): context.argument( "child_list", options_list=["--child-list", "--cl"], - help="Child device list (comma separated) includes only non-edge devices.", + help="Child device list (comma separated).", ) context.argument( "force", options_list=["--force", "-f"], - help="Overwrites the non-edge device's parent device.", + help="Overwrites the child device's parent device.", ) with self.argument_context("iot hub device-identity remove-children") as context: @@ -491,8 +507,41 @@ def load_arguments(self, _): context.argument( "child_list", options_list=["--child-list", "--cl"], - help="Child device list (comma separated) includes only non-edge devices.", + 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", + 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.", + ) + + with self.argument_context("iot hub device-identity children") as context: + context.argument("device_id", help="Id of edge device.") + context.argument("child_list", arg_type=children_list_prop_type) + + with self.argument_context("iot hub device-identity children add") as context: + context.argument( + "force", + options_list=["--force", "-f"], + help="Overwrites the child device's parent device.", + ) + + with self.argument_context("iot hub device-identity children remove") as context: context.argument( "remove_all", options_list=["--remove-all", "-a"], @@ -513,9 +562,6 @@ def load_arguments(self, _): "a percentage. Only values from 0 to 100 (inclusive) are permitted.", ) - 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 query") as context: context.argument( "query_command", diff --git a/azext_iot/central/providers/devicetwin_provider.py b/azext_iot/central/providers/devicetwin_provider.py index a44b3a6a9..836676742 100644 --- a/azext_iot/central/providers/devicetwin_provider.py +++ b/azext_iot/central/providers/devicetwin_provider.py @@ -52,7 +52,7 @@ def get_device_twin(self, central_dns_suffix): auth = BasicSasTokenAuthentication(sas_token=sas_token) service_sdk = SdkResolver(target=target, auth_override=auth).get_sdk(SdkType.service_sdk) try: - return service_sdk.twin.get_device_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/commands.py b/azext_iot/commands.py index 66c3a82f1..1fe5317d5 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -50,11 +50,54 @@ def load_command_table(self, _): ) cmd_group.command("import", "iot_device_import") cmd_group.command("export", "iot_device_export") - cmd_group.command("add-children", "iot_device_children_add") - cmd_group.command("remove-children", "iot_device_children_remove") - cmd_group.command("list-children", "iot_device_children_list") - cmd_group.command("get-parent", "iot_device_get_parent") - cmd_group.command("set-parent", "iot_device_set_parent") + 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 + ) as cmd_group: + cmd_group.show_command("add", "iot_device_children_add") + cmd_group.show_command("remove", "iot_device_children_remove") + cmd_group.show_command("list", "iot_device_children_list") + + with self.command_group( + "iot hub device-identity parent", command_type=iothub_ops + ) as cmd_group: + cmd_group.show_command("show", "iot_device_get_parent") + cmd_group.show_command("set", "iot_device_set_parent") with self.command_group( "iot hub device-identity connection-string", command_type=iothub_ops diff --git a/azext_iot/constants.py b/azext_iot/constants.py index b02b5052e..428b4ece9 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.5" +VERSION = "0.10.6" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/iothub/providers/job.py b/azext_iot/iothub/providers/job.py index 06e7a5f61..f5d21377a 100644 --- a/azext_iot/iothub/providers/job.py +++ b/azext_iot/iothub/providers/job.py @@ -31,8 +31,8 @@ def _get(self, job_id, job_version=JobVersionType.v2): try: if job_version == JobVersionType.v2: - return service_sdk.job_client.get_job(id=job_id, raw=True).response.json() - return self._convert_v1_to_v2(service_sdk.job_client.get_import_export_job(id=job_id)) + return service_sdk.jobs.get_scheduled_job(id=job_id, raw=True).response.json() + return self._convert_v1_to_v2(service_sdk.jobs.get_import_export_job(id=job_id)) except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -50,8 +50,8 @@ def _cancel(self, job_id, job_version=JobVersionType.v2): try: if job_version == JobVersionType.v2: - return service_sdk.job_client.cancel_job(id=job_id, raw=True).response.json() - return service_sdk.job_client.cancel_import_export_job(id=job_id) + return service_sdk.jobs.cancel_scheduled_job(id=job_id, raw=True).response.json() + return service_sdk.jobs.cancel_import_export_job(id=job_id) except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -92,10 +92,10 @@ def _list(self, job_type=None, job_status=None, top=None, job_version=JobVersion try: if job_version == JobVersionType.v2: query = [job_type, job_status] - query_method = service_sdk.job_client.query_jobs + query_method = service_sdk.jobs.query_scheduled_jobs jobs_collection.extend(_execute_query(query, query_method, top)) elif job_version == JobVersionType.v1: - jobs_collection.extend(service_sdk.job_client.get_import_export_jobs()) + jobs_collection.extend(service_sdk.jobs.get_import_export_jobs()) jobs_collection = [self._convert_v1_to_v2(job) for job in jobs_collection] return jobs_collection @@ -191,7 +191,7 @@ def create( service_sdk = self.get_sdk(SdkType.service_sdk) try: - job_result = service_sdk.job_client.create_job(id=job_id, job_request=job_request, raw=True).response.json() + job_result = service_sdk.jobs.create_scheduled_job(id=job_id, job_request=job_request, raw=True).response.json() if wait: logger.info("Waiting for job finished state...") current_datetime = datetime.now() diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 47ccb9950..6ab078378 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -62,7 +62,7 @@ def iot_query( try: query_args = [query_command] - query_method = service_sdk.registry_manager.query_iot_hub + query_method = service_sdk.query.get_twins return _execute_query(query_args, query_method, top) except CloudError as e: @@ -87,7 +87,7 @@ def _iot_device_show(target, device_id): service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - device = service_sdk.registry_manager.get_device( + device = service_sdk.devices.get_identity( id=device_id, raw=True ).response.json() device["hub"] = target.get("entity") @@ -141,18 +141,21 @@ def iot_device_create( 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 edge_enabled: - if add_children: - for non_edge_device_id in add_children.split(","): - nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) - _validate_nonedge_device(nonedge_device) - _validate_parent_child_relation(nonedge_device, "-", force) - else: - if set_parent_id: - edge_device = _iot_device_show(target, set_parent_id) - _validate_edge_device(edge_device) - deviceScope = edge_device["deviceScope"] + 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) @@ -174,18 +177,18 @@ def iot_device_create( secondary_thumbprint, status, status_reason, - deviceScope, + deviceScope ) - output = service_sdk.registry_manager.create_or_update_device( + output = service_sdk.devices.create_or_update_identity( id=device_id, device=device ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) if add_children: - for non_edge_device_id in add_children.split(","): - nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) - _update_nonedge_devicescope(target, nonedge_device, output.device_scope) + 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 @@ -204,15 +207,29 @@ def _assemble_device( auth = _assemble_auth(auth_method, pk, sk) cap = DeviceCapabilities(iot_edge=edge_enabled) - device = Device( - device_id=device_id, - authentication=auth, - capabilities=cap, - status=status, - status_reason=status_reason, - device_scope=device_scope, - ) - return device + if edge_enabled: + parent_scope = [] + if device_scope: + parent_scope = [device_scope] + device = Device( + device_id=device_id, + authentication=auth, + capabilities=cap, + status=status, + status_reason=status_reason, + parent_scopes=parent_scope, + ) + return device + else: + device = Device( + device_id=device_id, + authentication=auth, + capabilities=cap, + status=status, + status_reason=status_reason, + device_scope=device_scope, + ) + return device def _assemble_auth(auth_method, pk, sk): @@ -325,7 +342,7 @@ def _iot_device_update(target, device_id, device): try: headers = {} headers["If-Match"] = '"{}"'.format(device.etag) - return service_sdk.registry_manager.create_or_update_device( + return service_sdk.devices.create_or_update_identity( id=device_id, device=device, custom_headers=headers @@ -351,7 +368,7 @@ def iot_device_delete( if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - service_sdk.registry_manager.delete_device( + service_sdk.devices.delete_identity( id=device_id, custom_headers=headers ) return @@ -404,11 +421,10 @@ def iot_device_get_parent( hub_name=hub_name, resource_group_name=resource_group_name, login=login ) child_device = _iot_device_show(target, device_id) - _validate_nonedge_device(child_device) _validate_child_device(child_device) - device_scope = child_device["deviceScope"] - parent_device_id = device_scope[ - len(DEVICE_DEVICESCOPE_PREFIX) : device_scope.rindex("-") + parent_scope = child_device["parentScopes"][0] + parent_device_id = parent_scope[ + len(DEVICE_DEVICESCOPE_PREFIX) : parent_scope.rindex("-") ] return _iot_device_show(target, parent_device_id) @@ -429,9 +445,9 @@ def iot_device_set_parent( parent_device = _iot_device_show(target, parent_id) _validate_edge_device(parent_device) child_device = _iot_device_show(target, device_id) - _validate_nonedge_device(child_device) - _validate_parent_child_relation(child_device, parent_device["deviceScope"], force) - _update_nonedge_devicescope(target, child_device, parent_device["deviceScope"]) + _validate_parent_child_relation(child_device, force) + + _update_device_parent(target, child_device, child_device["capabilities"]["iotEdge"], parent_device["deviceScope"]) def iot_device_children_add( @@ -450,16 +466,18 @@ def iot_device_children_add( devices = [] edge_device = _iot_device_show(target, device_id) _validate_edge_device(edge_device) - for non_edge_device_id in child_list.split(","): - nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) - _validate_nonedge_device(nonedge_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( - nonedge_device, edge_device["deviceScope"], force + child_device, force ) - devices.append(nonedge_device) + devices.append(child_device) for device in devices: - _update_nonedge_devicescope(target, device, edge_device["deviceScope"]) + _update_device_parent(target, device, device["capabilities"]["iotEdge"], edge_device["deviceScope"]) def iot_device_children_remove( @@ -486,31 +504,33 @@ def iot_device_children_remove( device_id ) ) - for non_edge_device_id in [str(x["deviceId"]) for x in result]: - nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) - devices.append(nonedge_device) + for child_device_id in [str(x["deviceId"]) for x in result]: + child_device = _iot_device_show(target, child_device_id.strip()) + devices.append(child_device) elif child_list: edge_device = _iot_device_show(target, device_id) _validate_edge_device(edge_device) - for non_edge_device_id in child_list.split(","): - nonedge_device = _iot_device_show(target, non_edge_device_id.strip()) - _validate_nonedge_device(nonedge_device) - _validate_child_device(nonedge_device) - if nonedge_device["deviceScope"] == edge_device["deviceScope"]: - devices.append(nonedge_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) + if child_device["parentScopes"] == [edge_device["deviceScope"]]: + devices.append(child_device) else: raise CLIError( 'The entered child device "{}" isn\'t assigned as a child of edge device "{}"'.format( - non_edge_device_id.strip(), device_id + child_device_id.strip(), device_id ) ) else: raise CLIError( - "Please specify comma-separated child list or use --remove-all to remove all children." + "Please specify child list or use --remove-all to remove all children." ) for device in devices: - _update_nonedge_devicescope(target, device) + _update_device_parent(target, device, device["capabilities"]["iotEdge"]) def iot_device_children_list( @@ -519,6 +539,17 @@ def iot_device_children_list( result = _iot_device_children_list( cmd, device_id, hub_name, resource_group_name, login ) + + 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) @@ -535,25 +566,33 @@ def _iot_device_children_list( ) device = _iot_device_show(target, device_id) _validate_edge_device(device) - query = "select * from devices where capabilities.iotEdge=false and deviceScope='{}'".format( + query = "select deviceId from devices where array_contains(parentScopes, '{}')".format( device["deviceScope"] ) return iot_query(cmd, query, hub_name, None, resource_group_name, login=login) -def _update_nonedge_devicescope(target, nonedge_device, deviceScope=""): +def _update_device_parent(target, device, is_edge, device_scope=None): resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - nonedge_device["deviceScope"] = deviceScope - etag = nonedge_device.get("etag", None) + if is_edge: + parent_scopes = [] + if device_scope: + parent_scopes = [device_scope] + device["parentScopes"] = parent_scopes + else: + if not device_scope: + device_scope = "" + device["deviceScope"] = device_scope + etag = device.get("etag", None) if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - service_sdk.registry_manager.create_or_update_device( - id=nonedge_device["deviceId"], - device=nonedge_device, + service_sdk.devices.create_or_update_identity( + id=device["deviceId"], + device=device, custom_headers=headers, ) return @@ -571,28 +610,25 @@ def _validate_edge_device(device): ) -def _validate_nonedge_device(device): - if device["capabilities"]["iotEdge"]: +def _validate_child_device(device): + if "parentScopes" not in device: raise CLIError( - 'The entered child device "{}" should be non-edge device.'.format( + 'Device "{}" doesn\'t support parent device functionality.'.format( device["deviceId"] ) ) - - -def _validate_child_device(device): - if "deviceScope" not in device or device["deviceScope"] == "": + if not device["parentScopes"]: raise CLIError( - 'Device "{}" doesn\'t support parent device functionality.'.format( + 'Device "{}" doesn\'t have any parent device.'.format( device["deviceId"] ) ) -def _validate_parent_child_relation(child_device, deviceScope, force): - if "deviceScope" not in child_device or child_device["deviceScope"] == "": +def _validate_parent_child_relation(child_device, force): + if "parentScopes" not in child_device or child_device["parentScopes"] == []: return - if child_device["deviceScope"] != deviceScope: + else: if not force: raise CLIError( "The entered device \"{}\" already has a parent device, please use '--force'" @@ -644,7 +680,7 @@ def iot_device_module_create( pk=primary_thumbprint, sk=secondary_thumbprint, ) - return service_sdk.registry_manager.create_or_update_module( + return service_sdk.modules.create_or_update_identity( id=device_id, mid=module_id, module=module ) except CloudError as e: @@ -681,7 +717,7 @@ def iot_device_module_update( if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.registry_manager.create_or_update_module( + return service_sdk.modules.create_or_update_identity( id=device_id, mid=module_id, module=updated_module, @@ -735,7 +771,7 @@ def iot_device_module_list( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - return service_sdk.registry_manager.get_modules_on_device(device_id)[:top] + return service_sdk.modules.get_modules_on_device(device_id)[:top] except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -755,7 +791,7 @@ def _iot_device_module_show(target, device_id, module_id): service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - module = service_sdk.registry_manager.get_module( + module = service_sdk.modules.get_identity( id=device_id, mid=module_id, raw=True ).response.json() module["hub"] = target.get("entity") @@ -782,7 +818,7 @@ def iot_device_module_delete( if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - service_sdk.registry_manager.delete_module( + service_sdk.modules.delete_identity( id=device_id, mid=module_id, custom_headers=headers ) return @@ -810,7 +846,7 @@ def _iot_device_module_twin_show(target, device_id, module_id): service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - return service_sdk.twin.get_module_twin( + return service_sdk.modules.get_twin( id=device_id, mid=module_id, raw=True ).response.json() except CloudError as e: @@ -845,7 +881,7 @@ def iot_device_module_twin_update( if parameters.get("tags"): verify["tags"] = dict verify_transform(parameters, verify) - return service_sdk.twin.update_module_twin( + return service_sdk.modules.update_twin( id=device_id, mid=module_id, device_twin_info=parameters, @@ -882,7 +918,7 @@ def iot_device_module_twin_replace( if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.twin.replace_module_twin( + return service_sdk.modules.replace_twin( id=device_id, mid=module_id, device_twin_info=target_json, @@ -1334,7 +1370,7 @@ def iot_hub_configuration_metric_show( metric_query = metric_collection[metric_id] query_args = [metric_query] - query_method = service_sdk.registry_manager.query_iot_hub + query_method = service_sdk.query.get_twins metric_result = _execute_query(query_args, query_method, None) @@ -1366,7 +1402,7 @@ def _iot_device_twin_show(target, device_id): service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - return service_sdk.twin.get_device_twin(id=device_id, raw=True).response.json() + return service_sdk.devices.get_twin(id=device_id, raw=True).response.json() except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -1407,7 +1443,7 @@ def iot_device_twin_update( if parameters.get("tags"): verify["tags"] = dict verify_transform(parameters, verify) - return service_sdk.twin.update_device_twin( + return service_sdk.devices.update_twin( id=device_id, device_twin_info=parameters, custom_headers=headers ) except CloudError as e: @@ -1433,7 +1469,7 @@ def iot_device_twin_replace( if etag: headers = {} headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.twin.replace_device_twin( + return service_sdk.devices.replace_twin( id=device_id, device_twin_info=target_json, custom_headers=headers ) raise LookupError("device twin etag not found") @@ -1489,7 +1525,7 @@ def iot_device_method( connect_timeout_in_seconds=timeout, payload=method_payload, ) - return service_sdk.device_method.invoke_device_method( + return service_sdk.devices.invoke_method( device_id=device_id, direct_method_request=method, timeout=timeout ) except CloudError as e: @@ -1546,7 +1582,7 @@ def iot_device_module_method( connect_timeout_in_seconds=timeout, payload=method_payload, ) - return service_sdk.device_method.invoke_module_method( + return service_sdk.modules.invoke_method( device_id=device_id, module_id=module_id, direct_method_request=method, @@ -2198,7 +2234,7 @@ def iot_c2d_message_purge( resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) - return service_sdk.registry_manager.purge_command_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): diff --git a/azext_iot/sdk/iothub/service/iot_hub_gateway_service_ap_is.py b/azext_iot/sdk/iothub/service/iot_hub_gateway_service_ap_is.py index 29822f160..202f8574b 100644 --- a/azext_iot/sdk/iothub/service/iot_hub_gateway_service_ap_is.py +++ b/azext_iot/sdk/iothub/service/iot_hub_gateway_service_ap_is.py @@ -14,12 +14,14 @@ from msrestazure import AzureConfiguration from .version import VERSION from .operations.configuration_operations import ConfigurationOperations -from .operations.registry_manager_operations import RegistryManagerOperations -from .operations.job_client_operations import JobClientOperations -from .operations.fault_injection_operations import FaultInjectionOperations -from .operations.twin_operations import TwinOperations +from .operations.statistics_operations import StatisticsOperations +from .operations.devices_operations import DevicesOperations +from .operations.bulk_registry_operations import BulkRegistryOperations +from .operations.query_operations import QueryOperations +from .operations.jobs_operations import JobsOperations +from .operations.cloud_to_device_messages_operations import CloudToDeviceMessagesOperations +from .operations.modules_operations import ModulesOperations from .operations.digital_twin_operations import DigitalTwinOperations -from .operations.device_method_operations import DeviceMethodOperations from . import models from azext_iot.constants import USER_AGENT @@ -59,18 +61,24 @@ class IotHubGatewayServiceAPIs(SDKClient): :ivar configuration: Configuration operations :vartype configuration: service.operations.ConfigurationOperations - :ivar registry_manager: RegistryManager operations - :vartype registry_manager: service.operations.RegistryManagerOperations - :ivar job_client: JobClient operations - :vartype job_client: service.operations.JobClientOperations - :ivar fault_injection: FaultInjection operations - :vartype fault_injection: service.operations.FaultInjectionOperations - :ivar twin: Twin operations - :vartype twin: service.operations.TwinOperations + :vartype configuration: azure.operations.ConfigurationOperations + :ivar statistics: Statistics operations + :vartype statistics: azure.operations.StatisticsOperations + :ivar devices: Devices operations + :vartype devices: azure.operations.DevicesOperations + :ivar bulk_registry: BulkRegistry operations + :vartype bulk_registry: azure.operations.BulkRegistryOperations + :ivar query: Query operations + :vartype query: azure.operations.QueryOperations + :ivar jobs: Jobs operations + :vartype jobs: azure.operations.JobsOperations + :ivar cloud_to_device_messages: CloudToDeviceMessages operations + :vartype cloud_to_device_messages: azure.operations.CloudToDeviceMessagesOperations + :ivar modules: Modules operations + :vartype modules: azure.operations.ModulesOperations :ivar digital_twin: DigitalTwin operations + :vartype digital_twin: azure.operations.DigitalTwinOperations :vartype digital_twin: service.operations.DigitalTwinOperations - :ivar device_method: DeviceMethod operations - :vartype device_method: service.operations.DeviceMethodOperations :param credentials: Credentials needed for the client to connect to Azure. :type credentials: :mod:`A msrestazure Credentials @@ -85,21 +93,25 @@ def __init__( super(IotHubGatewayServiceAPIs, self).__init__(self.config.credentials, self.config) client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2019-10-01' + self.api_version = '2020-09-30' self._serialize = Serializer(client_models) self._deserialize = Deserializer(client_models) self.configuration = ConfigurationOperations( self._client, self.config, self._serialize, self._deserialize) - self.registry_manager = RegistryManagerOperations( + self.statistics = StatisticsOperations( self._client, self.config, self._serialize, self._deserialize) - self.job_client = JobClientOperations( + self.devices = DevicesOperations( self._client, self.config, self._serialize, self._deserialize) - self.fault_injection = FaultInjectionOperations( + self.bulk_registry = BulkRegistryOperations( self._client, self.config, self._serialize, self._deserialize) - self.twin = TwinOperations( + self.query = QueryOperations( self._client, self.config, self._serialize, self._deserialize) - self.digital_twin = DigitalTwinOperations( + self.jobs = JobsOperations( + self._client, self.config, self._serialize, self._deserialize) + self.cloud_to_device_messages = CloudToDeviceMessagesOperations( self._client, self.config, self._serialize, self._deserialize) - self.device_method = DeviceMethodOperations( + self.modules = ModulesOperations( + self._client, self.config, self._serialize, self._deserialize) + self.digital_twin = DigitalTwinOperations( self._client, self.config, self._serialize, self._deserialize) diff --git a/azext_iot/sdk/iothub/service/models/__init__.py b/azext_iot/sdk/iothub/service/models/__init__.py index 1c994117b..2f0a44266 100644 --- a/azext_iot/sdk/iothub/service/models/__init__.py +++ b/azext_iot/sdk/iothub/service/models/__init__.py @@ -32,14 +32,6 @@ from .twin_py3 import Twin from .job_properties_py3 import JobProperties from .purge_message_queue_result_py3 import PurgeMessageQueueResult - from .fault_injection_connection_properties_py3 import FaultInjectionConnectionProperties - from .fault_injection_properties_py3 import FaultInjectionProperties - from .desired_state_py3 import DesiredState - from .reported_py3 import Reported - from .desired_py3 import Desired - from .property_py3 import Property - from .interface_py3 import Interface - from .digital_twin_interfaces_py3 import DigitalTwinInterfaces from .cloud_to_device_method_py3 import CloudToDeviceMethod from .job_request_py3 import JobRequest from .device_job_statistics_py3 import DeviceJobStatistics @@ -47,10 +39,6 @@ from .query_result_py3 import QueryResult from .module_py3 import Module from .cloud_to_device_method_result_py3 import CloudToDeviceMethodResult - from .digital_twin_interfaces_patch_interfaces_value_properties_value_desired_py3 import DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired - from .digital_twin_interfaces_patch_interfaces_value_properties_value_py3 import DigitalTwinInterfacesPatchInterfacesValuePropertiesValue - from .digital_twin_interfaces_patch_interfaces_value_py3 import DigitalTwinInterfacesPatchInterfacesValue - from .digital_twin_interfaces_patch_py3 import DigitalTwinInterfacesPatch except (SyntaxError, ImportError): from .configuration_metrics import ConfigurationMetrics from .configuration_content import ConfigurationContent @@ -74,14 +62,6 @@ from .twin import Twin from .job_properties import JobProperties from .purge_message_queue_result import PurgeMessageQueueResult - from .fault_injection_connection_properties import FaultInjectionConnectionProperties - from .fault_injection_properties import FaultInjectionProperties - from .desired_state import DesiredState - from .reported import Reported - from .desired import Desired - from .property import Property - from .interface import Interface - from .digital_twin_interfaces import DigitalTwinInterfaces from .cloud_to_device_method import CloudToDeviceMethod from .job_request import JobRequest from .device_job_statistics import DeviceJobStatistics @@ -89,10 +69,6 @@ from .query_result import QueryResult from .module import Module from .cloud_to_device_method_result import CloudToDeviceMethodResult - from .digital_twin_interfaces_patch_interfaces_value_properties_value_desired import DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired - from .digital_twin_interfaces_patch_interfaces_value_properties_value import DigitalTwinInterfacesPatchInterfacesValuePropertiesValue - from .digital_twin_interfaces_patch_interfaces_value import DigitalTwinInterfacesPatchInterfacesValue - from .digital_twin_interfaces_patch import DigitalTwinInterfacesPatch __all__ = [ 'ConfigurationMetrics', @@ -117,14 +93,6 @@ 'Twin', 'JobProperties', 'PurgeMessageQueueResult', - 'FaultInjectionConnectionProperties', - 'FaultInjectionProperties', - 'DesiredState', - 'Reported', - 'Desired', - 'Property', - 'Interface', - 'DigitalTwinInterfaces', 'CloudToDeviceMethod', 'JobRequest', 'DeviceJobStatistics', @@ -132,8 +100,4 @@ 'QueryResult', 'Module', 'CloudToDeviceMethodResult', - 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired', - 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValue', - 'DigitalTwinInterfacesPatchInterfacesValue', - 'DigitalTwinInterfacesPatch', ] diff --git a/azext_iot/sdk/iothub/service/models/authentication_mechanism.py b/azext_iot/sdk/iothub/service/models/authentication_mechanism.py index a255e5e34..c7fd092b2 100644 --- a/azext_iot/sdk/iothub/service/models/authentication_mechanism.py +++ b/azext_iot/sdk/iothub/service/models/authentication_mechanism.py @@ -15,12 +15,15 @@ class AuthenticationMechanism(Model): """AuthenticationMechanism. - :param symmetric_key: + :param symmetric_key: The primary and secondary keys used for SAS based + authentication. :type symmetric_key: ~service.models.SymmetricKey - :param x509_thumbprint: + :param x509_thumbprint: The primary and secondary x509 thumbprints used + for x509 based authentication. :type x509_thumbprint: ~service.models.X509Thumbprint - :param type: Possible values include: 'sas', 'selfSigned', - 'certificateAuthority', 'none' + :param type: The type of authentication used to connect to the service. + Possible values include: 'sas', 'selfSigned', 'certificateAuthority', + 'none' :type type: str or ~service.models.enum """ diff --git a/azext_iot/sdk/iothub/service/models/authentication_mechanism_py3.py b/azext_iot/sdk/iothub/service/models/authentication_mechanism_py3.py index 42df1f0ba..4be15d9c0 100644 --- a/azext_iot/sdk/iothub/service/models/authentication_mechanism_py3.py +++ b/azext_iot/sdk/iothub/service/models/authentication_mechanism_py3.py @@ -15,12 +15,15 @@ class AuthenticationMechanism(Model): """AuthenticationMechanism. - :param symmetric_key: + :param symmetric_key: The primary and secondary keys used for SAS based + authentication. :type symmetric_key: ~service.models.SymmetricKey - :param x509_thumbprint: + :param x509_thumbprint: The primary and secondary x509 thumbprints used + for x509 based authentication. :type x509_thumbprint: ~service.models.X509Thumbprint - :param type: Possible values include: 'sas', 'selfSigned', - 'certificateAuthority', 'none' + :param type: The type of authentication used to connect to the service. + Possible values include: 'sas', 'selfSigned', 'certificateAuthority', + 'none' :type type: str or ~service.models.enum """ diff --git a/azext_iot/sdk/iothub/service/models/bulk_registry_operation_result.py b/azext_iot/sdk/iothub/service/models/bulk_registry_operation_result.py index e3588851f..255d647b1 100644 --- a/azext_iot/sdk/iothub/service/models/bulk_registry_operation_result.py +++ b/azext_iot/sdk/iothub/service/models/bulk_registry_operation_result.py @@ -13,15 +13,13 @@ class BulkRegistryOperationResult(Model): - """Encapsulates the result of a bulk registry operation. + """The result of the bulk registry operation. - :param is_successful: Whether or not the operation was successful. + :param is_successful: The operation result. :type is_successful: bool - :param errors: If the operation was not successful, this contains an array - of DeviceRegistryOperationError objects. + :param errors: The device registry operation errors. :type errors: list[~service.models.DeviceRegistryOperationError] - :param warnings: If the operation was partially successful, this contains - an array of DeviceRegistryOperationWarning objects. + :param warnings: The device registry operation warnings. :type warnings: list[~service.models.DeviceRegistryOperationWarning] """ diff --git a/azext_iot/sdk/iothub/service/models/bulk_registry_operation_result_py3.py b/azext_iot/sdk/iothub/service/models/bulk_registry_operation_result_py3.py index 54bdfeda7..ee3921b12 100644 --- a/azext_iot/sdk/iothub/service/models/bulk_registry_operation_result_py3.py +++ b/azext_iot/sdk/iothub/service/models/bulk_registry_operation_result_py3.py @@ -13,15 +13,13 @@ class BulkRegistryOperationResult(Model): - """Encapsulates the result of a bulk registry operation. + """The result of the bulk registry operation. - :param is_successful: Whether or not the operation was successful. + :param is_successful: The operation result. :type is_successful: bool - :param errors: If the operation was not successful, this contains an array - of DeviceRegistryOperationError objects. + :param errors: The device registry operation errors. :type errors: list[~service.models.DeviceRegistryOperationError] - :param warnings: If the operation was partially successful, this contains - an array of DeviceRegistryOperationWarning objects. + :param warnings: The device registry operation warnings. :type warnings: list[~service.models.DeviceRegistryOperationWarning] """ diff --git a/azext_iot/sdk/iothub/service/models/cloud_to_device_method.py b/azext_iot/sdk/iothub/service/models/cloud_to_device_method.py index ef6e75604..3be40fa66 100644 --- a/azext_iot/sdk/iothub/service/models/cloud_to_device_method.py +++ b/azext_iot/sdk/iothub/service/models/cloud_to_device_method.py @@ -13,11 +13,12 @@ class CloudToDeviceMethod(Model): - """Parameters to execute a direct method on the device. + """The parameters to execute a direct method on the device. - :param method_name: Method to run + :param method_name: The name of the method to execute. :type method_name: str - :param payload: Payload + :param payload: The JSON-formatted direct method payload, up to 128kb in + size. :type payload: object :param response_timeout_in_seconds: :type response_timeout_in_seconds: int diff --git a/azext_iot/sdk/iothub/service/models/cloud_to_device_method_py3.py b/azext_iot/sdk/iothub/service/models/cloud_to_device_method_py3.py index 59227e897..a6cae3425 100644 --- a/azext_iot/sdk/iothub/service/models/cloud_to_device_method_py3.py +++ b/azext_iot/sdk/iothub/service/models/cloud_to_device_method_py3.py @@ -13,11 +13,12 @@ class CloudToDeviceMethod(Model): - """Parameters to execute a direct method on the device. + """The parameters to execute a direct method on the device. - :param method_name: Method to run + :param method_name: The name of the method to execute. :type method_name: str - :param payload: Payload + :param payload: The JSON-formatted direct method payload, up to 128kb in + size. :type payload: object :param response_timeout_in_seconds: :type response_timeout_in_seconds: int diff --git a/azext_iot/sdk/iothub/service/models/configuration.py b/azext_iot/sdk/iothub/service/models/configuration.py index 7ef56c1bf..0f417804f 100644 --- a/azext_iot/sdk/iothub/service/models/configuration.py +++ b/azext_iot/sdk/iothub/service/models/configuration.py @@ -13,30 +13,33 @@ class Configuration(Model): - """Configuration for IotHub devices and modules. + """The configuration for Iot Hub device and module twins. - :param id: Gets Identifier for the configuration + :param id: The unique identifier of the configuration. :type id: str - :param schema_version: Gets Schema version for the configuration + :param schema_version: The schema version of the configuration. :type schema_version: str - :param labels: Gets or sets labels for the configuration + :param labels: The key-value pairs used to describe the configuration. :type labels: dict[str, str] - :param content: Gets or sets Content for the configuration + :param content: The content of the configuration. :type content: ~service.models.ConfigurationContent - :param target_condition: Gets or sets Target Condition for the - configuration + :param target_condition: The query used to define the targeted devices or + modules. The query is based on twin tags and/or reported properties. :type target_condition: str - :param created_time_utc: Gets creation time for the configuration + :param created_time_utc: The creation date and time of the configuration. :type created_time_utc: datetime - :param last_updated_time_utc: Gets last update time for the configuration + :param last_updated_time_utc: The update date and time of the + configuration. :type last_updated_time_utc: datetime - :param priority: Gets or sets Priority for the configuration + :param priority: The priority number assigned to the configuration. :type priority: int - :param system_metrics: System Configuration Metrics + :param system_metrics: The system metrics computed by the IoT Hub that + cannot be customized. :type system_metrics: ~service.models.ConfigurationMetrics - :param metrics: Custom Configuration Metrics + :param metrics: The custom metrics specified by the developer as queries + against twin reported properties. :type metrics: ~service.models.ConfigurationMetrics - :param etag: Gets or sets configuration's ETag + :param etag: The ETag of the configuration. :type etag: str """ diff --git a/azext_iot/sdk/iothub/service/models/configuration_content.py b/azext_iot/sdk/iothub/service/models/configuration_content.py index f480f1487..14f947b84 100644 --- a/azext_iot/sdk/iothub/service/models/configuration_content.py +++ b/azext_iot/sdk/iothub/service/models/configuration_content.py @@ -13,13 +13,13 @@ class ConfigurationContent(Model): - """Configuration Content for Devices or Modules on Edge Devices. + """The configuration content for devices or modules on edge devices. - :param device_content: Gets or sets device Configurations + :param device_content: The device configuration content. :type device_content: dict[str, object] - :param modules_content: Gets or sets Modules Configurations + :param modules_content: The modules configuration content. :type modules_content: dict[str, dict[str, object]] - :param module_content: Gets or sets Module Configurations + :param module_content: The module configuration content. :type module_content: dict[str, object] """ diff --git a/azext_iot/sdk/iothub/service/models/configuration_content_py3.py b/azext_iot/sdk/iothub/service/models/configuration_content_py3.py index 18773bce8..f26eab075 100644 --- a/azext_iot/sdk/iothub/service/models/configuration_content_py3.py +++ b/azext_iot/sdk/iothub/service/models/configuration_content_py3.py @@ -13,13 +13,13 @@ class ConfigurationContent(Model): - """Configuration Content for Devices or Modules on Edge Devices. + """The configuration content for devices or modules on edge devices. - :param device_content: Gets or sets device Configurations + :param device_content: The device configuration content. :type device_content: dict[str, object] - :param modules_content: Gets or sets Modules Configurations + :param modules_content: The modules configuration content. :type modules_content: dict[str, dict[str, object]] - :param module_content: Gets or sets Module Configurations + :param module_content: The module configuration content. :type module_content: dict[str, object] """ diff --git a/azext_iot/sdk/iothub/service/models/configuration_metrics.py b/azext_iot/sdk/iothub/service/models/configuration_metrics.py index 428834007..f37b821d6 100644 --- a/azext_iot/sdk/iothub/service/models/configuration_metrics.py +++ b/azext_iot/sdk/iothub/service/models/configuration_metrics.py @@ -13,11 +13,11 @@ class ConfigurationMetrics(Model): - """Configuration Metrics. + """The configuration metrics for Iot Hub devices and modules. - :param results: + :param results: The results of the metrics collection queries. :type results: dict[str, long] - :param queries: + :param queries: The key-value pairs with queries and their identifier. :type queries: dict[str, str] """ diff --git a/azext_iot/sdk/iothub/service/models/configuration_metrics_py3.py b/azext_iot/sdk/iothub/service/models/configuration_metrics_py3.py index f5afd42a7..546cd3cf6 100644 --- a/azext_iot/sdk/iothub/service/models/configuration_metrics_py3.py +++ b/azext_iot/sdk/iothub/service/models/configuration_metrics_py3.py @@ -13,11 +13,11 @@ class ConfigurationMetrics(Model): - """Configuration Metrics. + """The configuration metrics for Iot Hub devices and modules. - :param results: + :param results: The results of the metrics collection queries. :type results: dict[str, long] - :param queries: + :param queries: The key-value pairs with queries and their identifier. :type queries: dict[str, str] """ diff --git a/azext_iot/sdk/iothub/service/models/configuration_py3.py b/azext_iot/sdk/iothub/service/models/configuration_py3.py index 8d7794a31..bd0424488 100644 --- a/azext_iot/sdk/iothub/service/models/configuration_py3.py +++ b/azext_iot/sdk/iothub/service/models/configuration_py3.py @@ -13,30 +13,33 @@ class Configuration(Model): - """Configuration for IotHub devices and modules. + """The configuration for Iot Hub device and module twins. - :param id: Gets Identifier for the configuration + :param id: The unique identifier of the configuration. :type id: str - :param schema_version: Gets Schema version for the configuration + :param schema_version: The schema version of the configuration. :type schema_version: str - :param labels: Gets or sets labels for the configuration + :param labels: The key-value pairs used to describe the configuration. :type labels: dict[str, str] - :param content: Gets or sets Content for the configuration + :param content: The content of the configuration. :type content: ~service.models.ConfigurationContent - :param target_condition: Gets or sets Target Condition for the - configuration + :param target_condition: The query used to define the targeted devices or + modules. The query is based on twin tags and/or reported properties. :type target_condition: str - :param created_time_utc: Gets creation time for the configuration + :param created_time_utc: The creation date and time of the configuration. :type created_time_utc: datetime - :param last_updated_time_utc: Gets last update time for the configuration + :param last_updated_time_utc: The update date and time of the + configuration. :type last_updated_time_utc: datetime - :param priority: Gets or sets Priority for the configuration + :param priority: The priority number assigned to the configuration. :type priority: int - :param system_metrics: System Configuration Metrics + :param system_metrics: The system metrics computed by the IoT Hub that + cannot be customized. :type system_metrics: ~service.models.ConfigurationMetrics - :param metrics: Custom Configuration Metrics + :param metrics: The custom metrics specified by the developer as queries + against twin reported properties. :type metrics: ~service.models.ConfigurationMetrics - :param etag: Gets or sets configuration's ETag + :param etag: The ETag of the configuration. :type etag: str """ diff --git a/azext_iot/sdk/iothub/service/models/configuration_queries_test_input.py b/azext_iot/sdk/iothub/service/models/configuration_queries_test_input.py index faaaa3c20..c0669c47a 100644 --- a/azext_iot/sdk/iothub/service/models/configuration_queries_test_input.py +++ b/azext_iot/sdk/iothub/service/models/configuration_queries_test_input.py @@ -15,9 +15,11 @@ class ConfigurationQueriesTestInput(Model): """ConfigurationQueriesTestInput. - :param target_condition: + :param target_condition: The query used to define targeted devices or + modules. The query is based on twin tags and/or reported properties. :type target_condition: str - :param custom_metric_queries: + :param custom_metric_queries: The key-value pairs with queries and their + identifier. :type custom_metric_queries: dict[str, str] """ diff --git a/azext_iot/sdk/iothub/service/models/configuration_queries_test_input_py3.py b/azext_iot/sdk/iothub/service/models/configuration_queries_test_input_py3.py index 9723b0ad5..ef88e79de 100644 --- a/azext_iot/sdk/iothub/service/models/configuration_queries_test_input_py3.py +++ b/azext_iot/sdk/iothub/service/models/configuration_queries_test_input_py3.py @@ -15,9 +15,11 @@ class ConfigurationQueriesTestInput(Model): """ConfigurationQueriesTestInput. - :param target_condition: + :param target_condition: The query used to define targeted devices or + modules. The query is based on twin tags and/or reported properties. :type target_condition: str - :param custom_metric_queries: + :param custom_metric_queries: The key-value pairs with queries and their + identifier. :type custom_metric_queries: dict[str, str] """ diff --git a/azext_iot/sdk/iothub/service/models/configuration_queries_test_response.py b/azext_iot/sdk/iothub/service/models/configuration_queries_test_response.py index 7914218ea..9fb4ef83c 100644 --- a/azext_iot/sdk/iothub/service/models/configuration_queries_test_response.py +++ b/azext_iot/sdk/iothub/service/models/configuration_queries_test_response.py @@ -15,9 +15,11 @@ class ConfigurationQueriesTestResponse(Model): """ConfigurationQueriesTestResponse. - :param target_condition_error: + :param target_condition_error: The errors from running the target + condition query. :type target_condition_error: str - :param custom_metric_query_errors: + :param custom_metric_query_errors: The errors from running the custom + metric query. :type custom_metric_query_errors: dict[str, str] """ diff --git a/azext_iot/sdk/iothub/service/models/configuration_queries_test_response_py3.py b/azext_iot/sdk/iothub/service/models/configuration_queries_test_response_py3.py index 462e36e3b..cbd8978d0 100644 --- a/azext_iot/sdk/iothub/service/models/configuration_queries_test_response_py3.py +++ b/azext_iot/sdk/iothub/service/models/configuration_queries_test_response_py3.py @@ -15,9 +15,11 @@ class ConfigurationQueriesTestResponse(Model): """ConfigurationQueriesTestResponse. - :param target_condition_error: + :param target_condition_error: The errors from running the target + condition query. :type target_condition_error: str - :param custom_metric_query_errors: + :param custom_metric_query_errors: The errors from running the custom + metric query. :type custom_metric_query_errors: dict[str, str] """ diff --git a/azext_iot/sdk/iothub/service/models/desired.py b/azext_iot/sdk/iothub/service/models/desired.py deleted file mode 100644 index 5bc2a8dd8..000000000 --- a/azext_iot/sdk/iothub/service/models/desired.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Desired(Model): - """Desired. - - :param value: The desired value of the interface property to set in a - digitalTwin. - :type value: object - """ - - _attribute_map = { - 'value': {'key': 'value', 'type': 'object'}, - } - - def __init__(self, **kwargs): - super(Desired, self).__init__(**kwargs) - self.value = kwargs.get('value', None) diff --git a/azext_iot/sdk/iothub/service/models/desired_py3.py b/azext_iot/sdk/iothub/service/models/desired_py3.py deleted file mode 100644 index 2231d878f..000000000 --- a/azext_iot/sdk/iothub/service/models/desired_py3.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Desired(Model): - """Desired. - - :param value: The desired value of the interface property to set in a - digitalTwin. - :type value: object - """ - - _attribute_map = { - 'value': {'key': 'value', 'type': 'object'}, - } - - def __init__(self, *, value=None, **kwargs) -> None: - super(Desired, self).__init__(**kwargs) - self.value = value diff --git a/azext_iot/sdk/iothub/service/models/desired_state.py b/azext_iot/sdk/iothub/service/models/desired_state.py deleted file mode 100644 index c44919521..000000000 --- a/azext_iot/sdk/iothub/service/models/desired_state.py +++ /dev/null @@ -1,36 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DesiredState(Model): - """DesiredState. - - :param code: Status code for the operation. - :type code: int - :param version: Version of the desired value received. - :type version: long - :param description: Description of the status. - :type description: str - """ - - _attribute_map = { - 'code': {'key': 'code', 'type': 'int'}, - 'version': {'key': 'version', 'type': 'long'}, - 'description': {'key': 'description', 'type': 'str'}, - } - - def __init__(self, **kwargs): - super(DesiredState, self).__init__(**kwargs) - self.code = kwargs.get('code', None) - self.version = kwargs.get('version', None) - self.description = kwargs.get('description', None) diff --git a/azext_iot/sdk/iothub/service/models/desired_state_py3.py b/azext_iot/sdk/iothub/service/models/desired_state_py3.py deleted file mode 100644 index 4c788ea6a..000000000 --- a/azext_iot/sdk/iothub/service/models/desired_state_py3.py +++ /dev/null @@ -1,36 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DesiredState(Model): - """DesiredState. - - :param code: Status code for the operation. - :type code: int - :param version: Version of the desired value received. - :type version: long - :param description: Description of the status. - :type description: str - """ - - _attribute_map = { - 'code': {'key': 'code', 'type': 'int'}, - 'version': {'key': 'version', 'type': 'long'}, - 'description': {'key': 'description', 'type': 'str'}, - } - - def __init__(self, *, code: int=None, version: int=None, description: str=None, **kwargs) -> None: - super(DesiredState, self).__init__(**kwargs) - self.code = code - self.version = version - self.description = description diff --git a/azext_iot/sdk/iothub/service/models/device.py b/azext_iot/sdk/iothub/service/models/device.py index 550a34a97..21b01b9b9 100644 --- a/azext_iot/sdk/iothub/service/models/device.py +++ b/azext_iot/sdk/iothub/service/models/device.py @@ -15,33 +15,49 @@ class Device(Model): """Device. - :param device_id: + :param device_id: The unique identifier of the device. :type device_id: str - :param generation_id: + :param generation_id: The IoT Hub-generated, case-sensitive string up to + 128 characters long. This value is used to distinguish devices with the + same deviceId, when they have been deleted and re-created. :type generation_id: str - :param etag: + :param etag: The string representing a weak ETag for the device identity, + as per RFC7232. :type etag: str - :param connection_state: Possible values include: 'Disconnected', - 'Connected' + :param connection_state: The state of the device. Possible values include: + 'Disconnected', 'Connected' :type connection_state: str or ~service.models.enum - :param status: Possible values include: 'enabled', 'disabled' + :param status: The status of the device. If the status disabled, a device + cannot connect to the service. Possible values include: 'enabled', + 'disabled' :type status: str or ~service.models.enum - :param status_reason: + :param status_reason: The 128 character-long string that stores the reason + for the device identity status. All UTF-8 characters are allowed. :type status_reason: str - :param connection_state_updated_time: + :param connection_state_updated_time: The date and time the connection + state was last updated. :type connection_state_updated_time: datetime - :param status_updated_time: + :param status_updated_time: The date and time when the status field was + last updated. :type status_updated_time: datetime - :param last_activity_time: + :param last_activity_time: The date and last time the device last + connected, received, or sent a message. :type last_activity_time: datetime - :param cloud_to_device_message_count: + :param cloud_to_device_message_count: The number of cloud-to-device + messages currently queued to be sent to the device. :type cloud_to_device_message_count: int - :param authentication: + :param authentication: The authentication mechanism used by the device. :type authentication: ~service.models.AuthenticationMechanism - :param capabilities: + :param capabilities: The set of capabilities of the device. For example, + if this device is an edge device or not. :type capabilities: ~service.models.DeviceCapabilities - :param device_scope: + :param device_scope: The scope of the device. Auto generated and immutable + for edge devices and modifiable in leaf devices to create child/parent + relationship. :type device_scope: str + :param parent_scopes: The scopes of the upper level edge devices if + applicable. Only available for edge devices. + :type parent_scopes: list[str] """ _attribute_map = { @@ -58,6 +74,7 @@ class Device(Model): 'authentication': {'key': 'authentication', 'type': 'AuthenticationMechanism'}, 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, 'device_scope': {'key': 'deviceScope', 'type': 'str'}, + 'parent_scopes': {'key': 'parentScopes', 'type': '[str]'}, } def __init__(self, **kwargs): @@ -75,3 +92,4 @@ def __init__(self, **kwargs): self.authentication = kwargs.get('authentication', None) self.capabilities = kwargs.get('capabilities', None) self.device_scope = kwargs.get('device_scope', None) + self.parent_scopes = kwargs.get('parent_scopes', None) diff --git a/azext_iot/sdk/iothub/service/models/device_capabilities.py b/azext_iot/sdk/iothub/service/models/device_capabilities.py index bfa503203..4b07f0986 100644 --- a/azext_iot/sdk/iothub/service/models/device_capabilities.py +++ b/azext_iot/sdk/iothub/service/models/device_capabilities.py @@ -13,9 +13,10 @@ class DeviceCapabilities(Model): - """Status of Capabilities enabled on the device. + """The status of capabilities enabled on the device. - :param iot_edge: + :param iot_edge: The property that determines if the device is an edge + device or not. :type iot_edge: bool """ diff --git a/azext_iot/sdk/iothub/service/models/device_capabilities_py3.py b/azext_iot/sdk/iothub/service/models/device_capabilities_py3.py index 3df4b2e26..3cbf0978a 100644 --- a/azext_iot/sdk/iothub/service/models/device_capabilities_py3.py +++ b/azext_iot/sdk/iothub/service/models/device_capabilities_py3.py @@ -13,9 +13,10 @@ class DeviceCapabilities(Model): - """Status of Capabilities enabled on the device. + """The status of capabilities enabled on the device. - :param iot_edge: + :param iot_edge: The property that determines if the device is an edge + device or not. :type iot_edge: bool """ diff --git a/azext_iot/sdk/iothub/service/models/device_job_statistics.py b/azext_iot/sdk/iothub/service/models/device_job_statistics.py index b5ed1fcf0..e8cae4be0 100644 --- a/azext_iot/sdk/iothub/service/models/device_job_statistics.py +++ b/azext_iot/sdk/iothub/service/models/device_job_statistics.py @@ -13,17 +13,17 @@ class DeviceJobStatistics(Model): - """The job counts, e.g., number of failed/succeeded devices. + """The job statistics regarding execution status. - :param device_count: Number of devices in the job + :param device_count: The number of devices targeted by the job. :type device_count: int - :param failed_count: The number of failed jobs + :param failed_count: The number of failed jobs. :type failed_count: int - :param succeeded_count: The number of Successed jobs + :param succeeded_count: The number of succeeded jobs. :type succeeded_count: int - :param running_count: The number of running jobs + :param running_count: The number of running jobs. :type running_count: int - :param pending_count: The number of pending (scheduled) jobs + :param pending_count: The number of pending (scheduled) jobs. :type pending_count: int """ diff --git a/azext_iot/sdk/iothub/service/models/device_job_statistics_py3.py b/azext_iot/sdk/iothub/service/models/device_job_statistics_py3.py index a477aae4c..9b553328e 100644 --- a/azext_iot/sdk/iothub/service/models/device_job_statistics_py3.py +++ b/azext_iot/sdk/iothub/service/models/device_job_statistics_py3.py @@ -13,17 +13,17 @@ class DeviceJobStatistics(Model): - """The job counts, e.g., number of failed/succeeded devices. + """The job statistics regarding execution status. - :param device_count: Number of devices in the job + :param device_count: The number of devices targeted by the job. :type device_count: int - :param failed_count: The number of failed jobs + :param failed_count: The number of failed jobs. :type failed_count: int - :param succeeded_count: The number of Successed jobs + :param succeeded_count: The number of succeeded jobs. :type succeeded_count: int - :param running_count: The number of running jobs + :param running_count: The number of running jobs. :type running_count: int - :param pending_count: The number of pending (scheduled) jobs + :param pending_count: The number of pending (scheduled) jobs. :type pending_count: int """ diff --git a/azext_iot/sdk/iothub/service/models/device_py3.py b/azext_iot/sdk/iothub/service/models/device_py3.py index 8e92917a9..5a677d583 100644 --- a/azext_iot/sdk/iothub/service/models/device_py3.py +++ b/azext_iot/sdk/iothub/service/models/device_py3.py @@ -15,33 +15,49 @@ class Device(Model): """Device. - :param device_id: + :param device_id: The unique identifier of the device. :type device_id: str - :param generation_id: + :param generation_id: The IoT Hub-generated, case-sensitive string up to + 128 characters long. This value is used to distinguish devices with the + same deviceId, when they have been deleted and re-created. :type generation_id: str - :param etag: + :param etag: The string representing a weak ETag for the device identity, + as per RFC7232. :type etag: str - :param connection_state: Possible values include: 'Disconnected', - 'Connected' + :param connection_state: The state of the device. Possible values include: + 'Disconnected', 'Connected' :type connection_state: str or ~service.models.enum - :param status: Possible values include: 'enabled', 'disabled' + :param status: The status of the device. If the status disabled, a device + cannot connect to the service. Possible values include: 'enabled', + 'disabled' :type status: str or ~service.models.enum - :param status_reason: + :param status_reason: The 128 character-long string that stores the reason + for the device identity status. All UTF-8 characters are allowed. :type status_reason: str - :param connection_state_updated_time: + :param connection_state_updated_time: The date and time the connection + state was last updated. :type connection_state_updated_time: datetime - :param status_updated_time: + :param status_updated_time: The date and time when the status field was + last updated. :type status_updated_time: datetime - :param last_activity_time: + :param last_activity_time: The date and last time the device last + connected, received, or sent a message. :type last_activity_time: datetime - :param cloud_to_device_message_count: + :param cloud_to_device_message_count: The number of cloud-to-device + messages currently queued to be sent to the device. :type cloud_to_device_message_count: int - :param authentication: + :param authentication: The authentication mechanism used by the device. :type authentication: ~service.models.AuthenticationMechanism - :param capabilities: + :param capabilities: The set of capabilities of the device. For example, + if this device is an edge device or not. :type capabilities: ~service.models.DeviceCapabilities - :param device_scope: + :param device_scope: The scope of the device. Auto generated and immutable + for edge devices and modifiable in leaf devices to create child/parent + relationship. :type device_scope: str + :param parent_scopes: The scopes of the upper level edge devices if + applicable. Only available for edge devices. + :type parent_scopes: list[str] """ _attribute_map = { @@ -58,9 +74,10 @@ class Device(Model): 'authentication': {'key': 'authentication', 'type': 'AuthenticationMechanism'}, 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, 'device_scope': {'key': 'deviceScope', 'type': 'str'}, + 'parent_scopes': {'key': 'parentScopes', 'type': '[str]'}, } - def __init__(self, *, device_id: str=None, generation_id: str=None, etag: str=None, connection_state=None, status=None, status_reason: str=None, connection_state_updated_time=None, status_updated_time=None, last_activity_time=None, cloud_to_device_message_count: int=None, authentication=None, capabilities=None, device_scope: str=None, **kwargs) -> None: + def __init__(self, *, device_id: str=None, generation_id: str=None, etag: str=None, connection_state=None, status=None, status_reason: str=None, connection_state_updated_time=None, status_updated_time=None, last_activity_time=None, cloud_to_device_message_count: int=None, authentication=None, capabilities=None, device_scope: str=None, parent_scopes=None, **kwargs) -> None: super(Device, self).__init__(**kwargs) self.device_id = device_id self.generation_id = generation_id @@ -75,3 +92,4 @@ def __init__(self, *, device_id: str=None, generation_id: str=None, etag: str=No self.authentication = authentication self.capabilities = capabilities self.device_scope = device_scope + self.parent_scopes = parent_scopes diff --git a/azext_iot/sdk/iothub/service/models/device_registry_operation_error.py b/azext_iot/sdk/iothub/service/models/device_registry_operation_error.py index bf4e128f2..b042e9947 100644 --- a/azext_iot/sdk/iothub/service/models/device_registry_operation_error.py +++ b/azext_iot/sdk/iothub/service/models/device_registry_operation_error.py @@ -13,14 +13,14 @@ class DeviceRegistryOperationError(Model): - """Encapsulates device registry operation error details. + """The device registry operation error details. - :param device_id: The ID of the device that indicated the error. + :param device_id: The unique identifier of the device. :type device_id: str - :param error_code: ErrorCode associated with the error. Possible values - include: 'InvalidErrorCode', 'GenericBadRequest', - 'InvalidProtocolVersion', 'DeviceInvalidResultCount', 'InvalidOperation', - 'ArgumentInvalid', 'ArgumentNull', 'IotHubFormatError', + :param error_code: The error code. Possible values include: + 'InvalidErrorCode', 'GenericBadRequest', 'InvalidProtocolVersion', + 'DeviceInvalidResultCount', 'InvalidOperation', 'ArgumentInvalid', + 'ArgumentNull', 'IotHubFormatError', 'DeviceStorageEntitySerializationError', 'BlobContainerValidationError', 'ImportWarningExistsError', 'InvalidSchemaVersion', 'DeviceDefinedMultipleTimes', 'DeserializationError', @@ -32,7 +32,8 @@ class DeviceRegistryOperationError(Model): 'RequestTimedOut', 'UnsupportedOperationOnReplica', 'NullMessage', 'ConnectionForcefullyClosedOnNewConnection', 'InvalidDeviceScope', 'ConnectionForcefullyClosedOnFaultInjection', - 'ConnectionRejectedOnFaultInjection', 'InvalidRouteTestInput', + 'ConnectionRejectedOnFaultInjection', 'InvalidEndpointAuthenticationType', + 'ManagedIdentityNotEnabled', 'InvalidRouteTestInput', 'InvalidSourceOnRoute', 'RoutingNotEnabled', 'InvalidContentEncodingOrType', 'InvalidEndorsementKey', 'InvalidRegistrationId', 'InvalidStorageRootKey', @@ -46,8 +47,9 @@ class DeviceRegistryOperationError(Model): 'InvalidConfigurationCustomMetricsQuery', 'InvalidPnPInterfaceDefinition', 'InvalidPnPDesiredProperties', 'InvalidPnPReportedProperties', 'InvalidPnPWritableReportedProperties', 'InvalidDigitalTwinJsonPatch', - 'GenericUnauthorized', 'IotHubNotFound', 'IotHubUnauthorizedAccess', - 'IotHubUnauthorized', 'ElasticPoolNotFound', + 'InvalidDigitalTwinPayload', 'InvalidDigitalTwinPatch', + 'InvalidDigitalTwinPatchPath', 'GenericUnauthorized', 'IotHubNotFound', + 'IotHubUnauthorizedAccess', 'IotHubUnauthorized', 'ElasticPoolNotFound', 'SystemModuleModifyUnauthorizedAccess', 'GenericForbidden', 'IotHubSuspended', 'IotHubQuotaExceeded', 'JobQuotaExceeded', 'DeviceMaximumQueueDepthExceeded', 'IotHubMaxCbsTokenExceeded', @@ -105,7 +107,9 @@ class DeviceRegistryOperationError(Model): 'UnexpectedPropertyValue', 'OrchestrationOperationFailed', 'ModelRepoEndpointError', 'ResolutionError', 'UnableToFetchCredentials', 'UnableToFetchTenantInfo', 'UnableToShareIdentity', - 'UnableToExpandDiscoveryInfo', 'GenericBadGateway', + 'UnableToExpandDiscoveryInfo', 'UnableToExpandComponentInfo', + 'UnableToCompressComponentInfo', 'UnableToCompressDiscoveryInfo', + 'OrphanDiscoveryDocument', 'GenericBadGateway', 'InvalidResponseWhileProxying', 'GenericServiceUnavailable', 'ServiceUnavailable', 'PartitionNotFound', 'IotHubActivationFailed', 'ServerBusy', 'IotHubRestoring', 'ReceiveLinkOpensThrottled', @@ -113,11 +117,11 @@ class DeviceRegistryOperationError(Model): 'GroupNotAvailable', 'HostingServiceNotAvailable', 'GenericGatewayTimeout', 'GatewayTimeout' :type error_code: str or ~service.models.enum - :param error_status: Additional details associated with the error. + :param error_status: The details of the error. :type error_status: str - :param module_id: + :param module_id: The unique identifier of the module, if applicable. :type module_id: str - :param operation: + :param operation: The type of the operation that failed. :type operation: str """ diff --git a/azext_iot/sdk/iothub/service/models/device_registry_operation_error_py3.py b/azext_iot/sdk/iothub/service/models/device_registry_operation_error_py3.py index 598439a79..2b5a95e19 100644 --- a/azext_iot/sdk/iothub/service/models/device_registry_operation_error_py3.py +++ b/azext_iot/sdk/iothub/service/models/device_registry_operation_error_py3.py @@ -13,14 +13,14 @@ class DeviceRegistryOperationError(Model): - """Encapsulates device registry operation error details. + """The device registry operation error details. - :param device_id: The ID of the device that indicated the error. + :param device_id: The unique identifier of the device. :type device_id: str - :param error_code: ErrorCode associated with the error. Possible values - include: 'InvalidErrorCode', 'GenericBadRequest', - 'InvalidProtocolVersion', 'DeviceInvalidResultCount', 'InvalidOperation', - 'ArgumentInvalid', 'ArgumentNull', 'IotHubFormatError', + :param error_code: The error code. Possible values include: + 'InvalidErrorCode', 'GenericBadRequest', 'InvalidProtocolVersion', + 'DeviceInvalidResultCount', 'InvalidOperation', 'ArgumentInvalid', + 'ArgumentNull', 'IotHubFormatError', 'DeviceStorageEntitySerializationError', 'BlobContainerValidationError', 'ImportWarningExistsError', 'InvalidSchemaVersion', 'DeviceDefinedMultipleTimes', 'DeserializationError', @@ -32,7 +32,8 @@ class DeviceRegistryOperationError(Model): 'RequestTimedOut', 'UnsupportedOperationOnReplica', 'NullMessage', 'ConnectionForcefullyClosedOnNewConnection', 'InvalidDeviceScope', 'ConnectionForcefullyClosedOnFaultInjection', - 'ConnectionRejectedOnFaultInjection', 'InvalidRouteTestInput', + 'ConnectionRejectedOnFaultInjection', 'InvalidEndpointAuthenticationType', + 'ManagedIdentityNotEnabled', 'InvalidRouteTestInput', 'InvalidSourceOnRoute', 'RoutingNotEnabled', 'InvalidContentEncodingOrType', 'InvalidEndorsementKey', 'InvalidRegistrationId', 'InvalidStorageRootKey', @@ -46,8 +47,9 @@ class DeviceRegistryOperationError(Model): 'InvalidConfigurationCustomMetricsQuery', 'InvalidPnPInterfaceDefinition', 'InvalidPnPDesiredProperties', 'InvalidPnPReportedProperties', 'InvalidPnPWritableReportedProperties', 'InvalidDigitalTwinJsonPatch', - 'GenericUnauthorized', 'IotHubNotFound', 'IotHubUnauthorizedAccess', - 'IotHubUnauthorized', 'ElasticPoolNotFound', + 'InvalidDigitalTwinPayload', 'InvalidDigitalTwinPatch', + 'InvalidDigitalTwinPatchPath', 'GenericUnauthorized', 'IotHubNotFound', + 'IotHubUnauthorizedAccess', 'IotHubUnauthorized', 'ElasticPoolNotFound', 'SystemModuleModifyUnauthorizedAccess', 'GenericForbidden', 'IotHubSuspended', 'IotHubQuotaExceeded', 'JobQuotaExceeded', 'DeviceMaximumQueueDepthExceeded', 'IotHubMaxCbsTokenExceeded', @@ -105,7 +107,9 @@ class DeviceRegistryOperationError(Model): 'UnexpectedPropertyValue', 'OrchestrationOperationFailed', 'ModelRepoEndpointError', 'ResolutionError', 'UnableToFetchCredentials', 'UnableToFetchTenantInfo', 'UnableToShareIdentity', - 'UnableToExpandDiscoveryInfo', 'GenericBadGateway', + 'UnableToExpandDiscoveryInfo', 'UnableToExpandComponentInfo', + 'UnableToCompressComponentInfo', 'UnableToCompressDiscoveryInfo', + 'OrphanDiscoveryDocument', 'GenericBadGateway', 'InvalidResponseWhileProxying', 'GenericServiceUnavailable', 'ServiceUnavailable', 'PartitionNotFound', 'IotHubActivationFailed', 'ServerBusy', 'IotHubRestoring', 'ReceiveLinkOpensThrottled', @@ -113,11 +117,11 @@ class DeviceRegistryOperationError(Model): 'GroupNotAvailable', 'HostingServiceNotAvailable', 'GenericGatewayTimeout', 'GatewayTimeout' :type error_code: str or ~service.models.enum - :param error_status: Additional details associated with the error. + :param error_status: The details of the error. :type error_status: str - :param module_id: + :param module_id: The unique identifier of the module, if applicable. :type module_id: str - :param operation: + :param operation: The type of the operation that failed. :type operation: str """ diff --git a/azext_iot/sdk/iothub/service/models/device_registry_operation_warning.py b/azext_iot/sdk/iothub/service/models/device_registry_operation_warning.py index 470907b68..e1cc3b556 100644 --- a/azext_iot/sdk/iothub/service/models/device_registry_operation_warning.py +++ b/azext_iot/sdk/iothub/service/models/device_registry_operation_warning.py @@ -13,14 +13,14 @@ class DeviceRegistryOperationWarning(Model): - """Encapsulates device registry operation error details. + """The device registry operation warning details. - :param device_id: The ID of the device that indicated the warning. + :param device_id: The unique identifier of the device. :type device_id: str - :param warning_code: Possible values include: + :param warning_code: The warning code. Possible values include: 'DeviceRegisteredWithoutTwin' :type warning_code: str or ~service.models.enum - :param warning_status: Additional details associated with the warning. + :param warning_status: The details of the warning. :type warning_status: str """ diff --git a/azext_iot/sdk/iothub/service/models/device_registry_operation_warning_py3.py b/azext_iot/sdk/iothub/service/models/device_registry_operation_warning_py3.py index 03ed5427a..1350e125d 100644 --- a/azext_iot/sdk/iothub/service/models/device_registry_operation_warning_py3.py +++ b/azext_iot/sdk/iothub/service/models/device_registry_operation_warning_py3.py @@ -13,14 +13,14 @@ class DeviceRegistryOperationWarning(Model): - """Encapsulates device registry operation error details. + """The device registry operation warning details. - :param device_id: The ID of the device that indicated the warning. + :param device_id: The unique identifier of the device. :type device_id: str - :param warning_code: Possible values include: + :param warning_code: The warning code. Possible values include: 'DeviceRegisteredWithoutTwin' :type warning_code: str or ~service.models.enum - :param warning_status: Additional details associated with the warning. + :param warning_status: The details of the warning. :type warning_status: str """ diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces.py deleted file mode 100644 index 09a6e4abb..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfaces(Model): - """DigitalTwinInterfaces. - - :param interfaces: Interface(s) data on the digital twin. - :type interfaces: dict[str, ~service.models.Interface] - :param version: Version of digital twin. - :type version: long - """ - - _attribute_map = { - 'interfaces': {'key': 'interfaces', 'type': '{Interface}'}, - 'version': {'key': 'version', 'type': 'long'}, - } - - def __init__(self, **kwargs): - super(DigitalTwinInterfaces, self).__init__(**kwargs) - self.interfaces = kwargs.get('interfaces', None) - self.version = kwargs.get('version', None) diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch.py deleted file mode 100644 index 266a62caf..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatch(Model): - """DigitalTwinInterfacesPatch. - - :param interfaces: Interface(s) data to patch in the digital twin. - :type interfaces: dict[str, - ~service.models.DigitalTwinInterfacesPatchInterfacesValue] - """ - - _attribute_map = { - 'interfaces': {'key': 'interfaces', 'type': '{DigitalTwinInterfacesPatchInterfacesValue}'}, - } - - def __init__(self, **kwargs): - super(DigitalTwinInterfacesPatch, self).__init__(**kwargs) - self.interfaces = kwargs.get('interfaces', None) diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value.py deleted file mode 100644 index f710ec229..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatchInterfacesValue(Model): - """DigitalTwinInterfacesPatchInterfacesValue. - - :param properties: List of properties to update in an interface. - :type properties: dict[str, - ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValue] - """ - - _attribute_map = { - 'properties': {'key': 'properties', 'type': '{DigitalTwinInterfacesPatchInterfacesValuePropertiesValue}'}, - } - - def __init__(self, **kwargs): - super(DigitalTwinInterfacesPatchInterfacesValue, self).__init__(**kwargs) - self.properties = kwargs.get('properties', None) diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py deleted file mode 100644 index 40a4c9e43..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatchInterfacesValuePropertiesValue(Model): - """DigitalTwinInterfacesPatchInterfacesValuePropertiesValue. - - :param desired: - :type desired: - ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired - """ - - _attribute_map = { - 'desired': {'key': 'desired', 'type': 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired'}, - } - - def __init__(self, **kwargs): - super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValue, self).__init__(**kwargs) - self.desired = kwargs.get('desired', None) diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py deleted file mode 100644 index 24f07c3d8..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired(Model): - """DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired. - - :param value: The desired value of the interface property to set in a - digitalTwin. - :type value: object - """ - - _attribute_map = { - 'value': {'key': 'value', 'type': 'object'}, - } - - def __init__(self, **kwargs): - super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired, self).__init__(**kwargs) - self.value = kwargs.get('value', None) diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired_py3.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired_py3.py deleted file mode 100644 index 2bf2325b3..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired_py3.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired(Model): - """DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired. - - :param value: The desired value of the interface property to set in a - digitalTwin. - :type value: object - """ - - _attribute_map = { - 'value': {'key': 'value', 'type': 'object'}, - } - - def __init__(self, *, value=None, **kwargs) -> None: - super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired, self).__init__(**kwargs) - self.value = value diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_py3.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_py3.py deleted file mode 100644 index 3ca3eb190..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_properties_value_py3.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatchInterfacesValuePropertiesValue(Model): - """DigitalTwinInterfacesPatchInterfacesValuePropertiesValue. - - :param desired: - :type desired: - ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired - """ - - _attribute_map = { - 'desired': {'key': 'desired', 'type': 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired'}, - } - - def __init__(self, *, desired=None, **kwargs) -> None: - super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValue, self).__init__(**kwargs) - self.desired = desired diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_py3.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_py3.py deleted file mode 100644 index b0d87d685..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_interfaces_value_py3.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatchInterfacesValue(Model): - """DigitalTwinInterfacesPatchInterfacesValue. - - :param properties: List of properties to update in an interface. - :type properties: dict[str, - ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValue] - """ - - _attribute_map = { - 'properties': {'key': 'properties', 'type': '{DigitalTwinInterfacesPatchInterfacesValuePropertiesValue}'}, - } - - def __init__(self, *, properties=None, **kwargs) -> None: - super(DigitalTwinInterfacesPatchInterfacesValue, self).__init__(**kwargs) - self.properties = properties diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_py3.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_py3.py deleted file mode 100644 index 4529fd90f..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_patch_py3.py +++ /dev/null @@ -1,29 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfacesPatch(Model): - """DigitalTwinInterfacesPatch. - - :param interfaces: Interface(s) data to patch in the digital twin. - :type interfaces: dict[str, - ~service.models.DigitalTwinInterfacesPatchInterfacesValue] - """ - - _attribute_map = { - 'interfaces': {'key': 'interfaces', 'type': '{DigitalTwinInterfacesPatchInterfacesValue}'}, - } - - def __init__(self, *, interfaces=None, **kwargs) -> None: - super(DigitalTwinInterfacesPatch, self).__init__(**kwargs) - self.interfaces = interfaces diff --git a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_py3.py b/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_py3.py deleted file mode 100644 index db386d3cd..000000000 --- a/azext_iot/sdk/iothub/service/models/digital_twin_interfaces_py3.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class DigitalTwinInterfaces(Model): - """DigitalTwinInterfaces. - - :param interfaces: Interface(s) data on the digital twin. - :type interfaces: dict[str, ~service.models.Interface] - :param version: Version of digital twin. - :type version: long - """ - - _attribute_map = { - 'interfaces': {'key': 'interfaces', 'type': '{Interface}'}, - 'version': {'key': 'version', 'type': 'long'}, - } - - def __init__(self, *, interfaces=None, version: int=None, **kwargs) -> None: - super(DigitalTwinInterfaces, self).__init__(**kwargs) - self.interfaces = interfaces - self.version = version diff --git a/azext_iot/sdk/iothub/service/models/export_import_device.py b/azext_iot/sdk/iothub/service/models/export_import_device.py index 2d00b7cc2..335eecb8f 100644 --- a/azext_iot/sdk/iothub/service/models/export_import_device.py +++ b/azext_iot/sdk/iothub/service/models/export_import_device.py @@ -15,38 +15,46 @@ class ExportImportDevice(Model): """ExportImportDevice. - :param id: Device Id is always required + :param id: The unique identifier of the device. :type id: str - :param module_id: ModuleId is applicable to modules only + :param module_id: The unique identifier of the module, if applicable. :type module_id: str - :param e_tag: ETag parameter is only used for pre-conditioning the update - when importMode is updateIfMatchETag + :param e_tag: The string representing a weak ETag for the device RFC7232. + The value is only used if import mode is updateIfMatchETag, in that case + the import operation is performed only if this ETag matches the value + maintained by the server. :type e_tag: str - :param import_mode: Possible values include: 'create', 'update', - 'updateIfMatchETag', 'delete', 'deleteIfMatchETag', 'updateTwin', - 'updateTwinIfMatchETag' + :param import_mode: The type of registry operation and ETag preferences. + Possible values include: 'create', 'update', 'updateIfMatchETag', + 'delete', 'deleteIfMatchETag', 'updateTwin', 'updateTwinIfMatchETag' :type import_mode: str or ~service.models.enum - :param status: Status is optional and defaults to enabled. Possible values - include: 'enabled', 'disabled' + :param status: The status of the module. If disabled, the module cannot + connect to the service. Possible values include: 'enabled', 'disabled' :type status: str or ~service.models.enum - :param status_reason: + :param status_reason: The 128 character-long string that stores the reason + for the device identity status. All UTF-8 characters are allowed. :type status_reason: str - :param authentication: Authentication parameter is optional and defaults - to SAS if not provided. In that case, we auto-generate primary/secondary - access keys + :param authentication: The authentication mechanism used by the module. + This parameter is optional and defaults to SAS if not provided. In that + case, primary/secondary access keys are auto-generated. :type authentication: ~service.models.AuthenticationMechanism - :param twin_etag: twinETag parameter is only used for pre-conditioning the - update when importMode is updateTwinIfMatchETag + :param twin_etag: The string representing a weak ETag for the device twin + RFC7232. The value is only used if import mode is updateIfMatchETag, in + that case the import operation is performed only if this ETag matches the + value maintained by the server. :type twin_etag: str - :param tags: + :param tags: The JSON document read and written by the solution back end. + The tags are not visible to device apps. :type tags: dict[str, object] - :param properties: Properties are optional and defaults to empty object + :param properties: The desired and reported properties for the device. :type properties: ~service.models.PropertyContainer - :param capabilities: Capabilities param is optional and defaults to no - capability + :param capabilities: The status of capabilities enabled on the device. :type capabilities: ~service.models.DeviceCapabilities - :param device_scope: + :param device_scope: The scope of the device. :type device_scope: str + :param parent_scopes: The scopes of the upper level edge devices if + applicable. Only available for edge devices. + :type parent_scopes: list[str] """ _attribute_map = { @@ -62,6 +70,7 @@ class ExportImportDevice(Model): 'properties': {'key': 'properties', 'type': 'PropertyContainer'}, 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, 'device_scope': {'key': 'deviceScope', 'type': 'str'}, + 'parent_scopes': {'key': 'parentScopes', 'type': '[str]'}, } def __init__(self, **kwargs): @@ -78,3 +87,4 @@ def __init__(self, **kwargs): self.properties = kwargs.get('properties', None) self.capabilities = kwargs.get('capabilities', None) self.device_scope = kwargs.get('device_scope', None) + self.parent_scopes = kwargs.get('parent_scopes', None) diff --git a/azext_iot/sdk/iothub/service/models/export_import_device_py3.py b/azext_iot/sdk/iothub/service/models/export_import_device_py3.py index cb7da1440..39332be96 100644 --- a/azext_iot/sdk/iothub/service/models/export_import_device_py3.py +++ b/azext_iot/sdk/iothub/service/models/export_import_device_py3.py @@ -15,38 +15,46 @@ class ExportImportDevice(Model): """ExportImportDevice. - :param id: Device Id is always required + :param id: The unique identifier of the device. :type id: str - :param module_id: ModuleId is applicable to modules only + :param module_id: The unique identifier of the module, if applicable. :type module_id: str - :param e_tag: ETag parameter is only used for pre-conditioning the update - when importMode is updateIfMatchETag + :param e_tag: The string representing a weak ETag for the device RFC7232. + The value is only used if import mode is updateIfMatchETag, in that case + the import operation is performed only if this ETag matches the value + maintained by the server. :type e_tag: str - :param import_mode: Possible values include: 'create', 'update', - 'updateIfMatchETag', 'delete', 'deleteIfMatchETag', 'updateTwin', - 'updateTwinIfMatchETag' + :param import_mode: The type of registry operation and ETag preferences. + Possible values include: 'create', 'update', 'updateIfMatchETag', + 'delete', 'deleteIfMatchETag', 'updateTwin', 'updateTwinIfMatchETag' :type import_mode: str or ~service.models.enum - :param status: Status is optional and defaults to enabled. Possible values - include: 'enabled', 'disabled' + :param status: The status of the module. If disabled, the module cannot + connect to the service. Possible values include: 'enabled', 'disabled' :type status: str or ~service.models.enum - :param status_reason: + :param status_reason: The 128 character-long string that stores the reason + for the device identity status. All UTF-8 characters are allowed. :type status_reason: str - :param authentication: Authentication parameter is optional and defaults - to SAS if not provided. In that case, we auto-generate primary/secondary - access keys + :param authentication: The authentication mechanism used by the module. + This parameter is optional and defaults to SAS if not provided. In that + case, primary/secondary access keys are auto-generated. :type authentication: ~service.models.AuthenticationMechanism - :param twin_etag: twinETag parameter is only used for pre-conditioning the - update when importMode is updateTwinIfMatchETag + :param twin_etag: The string representing a weak ETag for the device twin + RFC7232. The value is only used if import mode is updateIfMatchETag, in + that case the import operation is performed only if this ETag matches the + value maintained by the server. :type twin_etag: str - :param tags: + :param tags: The JSON document read and written by the solution back end. + The tags are not visible to device apps. :type tags: dict[str, object] - :param properties: Properties are optional and defaults to empty object + :param properties: The desired and reported properties for the device. :type properties: ~service.models.PropertyContainer - :param capabilities: Capabilities param is optional and defaults to no - capability + :param capabilities: The status of capabilities enabled on the device. :type capabilities: ~service.models.DeviceCapabilities - :param device_scope: + :param device_scope: The scope of the device. :type device_scope: str + :param parent_scopes: The scopes of the upper level edge devices if + applicable. Only available for edge devices. + :type parent_scopes: list[str] """ _attribute_map = { @@ -62,9 +70,10 @@ class ExportImportDevice(Model): 'properties': {'key': 'properties', 'type': 'PropertyContainer'}, 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, 'device_scope': {'key': 'deviceScope', 'type': 'str'}, + 'parent_scopes': {'key': 'parentScopes', 'type': '[str]'}, } - def __init__(self, *, id: str=None, module_id: str=None, e_tag: str=None, import_mode=None, status=None, status_reason: str=None, authentication=None, twin_etag: str=None, tags=None, properties=None, capabilities=None, device_scope: str=None, **kwargs) -> None: + def __init__(self, *, id: str=None, module_id: str=None, e_tag: str=None, import_mode=None, status=None, status_reason: str=None, authentication=None, twin_etag: str=None, tags=None, properties=None, capabilities=None, device_scope: str=None, parent_scopes=None, **kwargs) -> None: super(ExportImportDevice, self).__init__(**kwargs) self.id = id self.module_id = module_id @@ -78,3 +87,4 @@ def __init__(self, *, id: str=None, module_id: str=None, e_tag: str=None, import self.properties = properties self.capabilities = capabilities self.device_scope = device_scope + self.parent_scopes = parent_scopes diff --git a/azext_iot/sdk/iothub/service/models/fault_injection_connection_properties.py b/azext_iot/sdk/iothub/service/models/fault_injection_connection_properties.py deleted file mode 100644 index 8c542d177..000000000 --- a/azext_iot/sdk/iothub/service/models/fault_injection_connection_properties.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class FaultInjectionConnectionProperties(Model): - """FaultInjectionConnectionProperties. - - :param action: Possible values include: 'None', 'CloseAll', 'Periodic' - :type action: str or ~service.models.enum - :param block_duration_in_minutes: - :type block_duration_in_minutes: int - """ - - _attribute_map = { - 'action': {'key': 'action', 'type': 'str'}, - 'block_duration_in_minutes': {'key': 'blockDurationInMinutes', 'type': 'int'}, - } - - def __init__(self, **kwargs): - super(FaultInjectionConnectionProperties, self).__init__(**kwargs) - self.action = kwargs.get('action', None) - self.block_duration_in_minutes = kwargs.get('block_duration_in_minutes', None) diff --git a/azext_iot/sdk/iothub/service/models/fault_injection_connection_properties_py3.py b/azext_iot/sdk/iothub/service/models/fault_injection_connection_properties_py3.py deleted file mode 100644 index 6fc2cca49..000000000 --- a/azext_iot/sdk/iothub/service/models/fault_injection_connection_properties_py3.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class FaultInjectionConnectionProperties(Model): - """FaultInjectionConnectionProperties. - - :param action: Possible values include: 'None', 'CloseAll', 'Periodic' - :type action: str or ~service.models.enum - :param block_duration_in_minutes: - :type block_duration_in_minutes: int - """ - - _attribute_map = { - 'action': {'key': 'action', 'type': 'str'}, - 'block_duration_in_minutes': {'key': 'blockDurationInMinutes', 'type': 'int'}, - } - - def __init__(self, *, action=None, block_duration_in_minutes: int=None, **kwargs) -> None: - super(FaultInjectionConnectionProperties, self).__init__(**kwargs) - self.action = action - self.block_duration_in_minutes = block_duration_in_minutes diff --git a/azext_iot/sdk/iothub/service/models/fault_injection_properties.py b/azext_iot/sdk/iothub/service/models/fault_injection_properties.py deleted file mode 100644 index be83309e5..000000000 --- a/azext_iot/sdk/iothub/service/models/fault_injection_properties.py +++ /dev/null @@ -1,36 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class FaultInjectionProperties(Model): - """FaultInjectionProperties. - - :param iot_hub_name: - :type iot_hub_name: str - :param connection: - :type connection: ~service.models.FaultInjectionConnectionProperties - :param last_updated_time_utc: Service generated. - :type last_updated_time_utc: datetime - """ - - _attribute_map = { - 'iot_hub_name': {'key': 'IotHubName', 'type': 'str'}, - 'connection': {'key': 'connection', 'type': 'FaultInjectionConnectionProperties'}, - 'last_updated_time_utc': {'key': 'lastUpdatedTimeUtc', 'type': 'iso-8601'}, - } - - def __init__(self, **kwargs): - super(FaultInjectionProperties, self).__init__(**kwargs) - self.iot_hub_name = kwargs.get('iot_hub_name', None) - self.connection = kwargs.get('connection', None) - self.last_updated_time_utc = kwargs.get('last_updated_time_utc', None) diff --git a/azext_iot/sdk/iothub/service/models/fault_injection_properties_py3.py b/azext_iot/sdk/iothub/service/models/fault_injection_properties_py3.py deleted file mode 100644 index 6d3b840dc..000000000 --- a/azext_iot/sdk/iothub/service/models/fault_injection_properties_py3.py +++ /dev/null @@ -1,36 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class FaultInjectionProperties(Model): - """FaultInjectionProperties. - - :param iot_hub_name: - :type iot_hub_name: str - :param connection: - :type connection: ~service.models.FaultInjectionConnectionProperties - :param last_updated_time_utc: Service generated. - :type last_updated_time_utc: datetime - """ - - _attribute_map = { - 'iot_hub_name': {'key': 'IotHubName', 'type': 'str'}, - 'connection': {'key': 'connection', 'type': 'FaultInjectionConnectionProperties'}, - 'last_updated_time_utc': {'key': 'lastUpdatedTimeUtc', 'type': 'iso-8601'}, - } - - def __init__(self, *, iot_hub_name: str=None, connection=None, last_updated_time_utc=None, **kwargs) -> None: - super(FaultInjectionProperties, self).__init__(**kwargs) - self.iot_hub_name = iot_hub_name - self.connection = connection - self.last_updated_time_utc = last_updated_time_utc diff --git a/azext_iot/sdk/iothub/service/models/interface.py b/azext_iot/sdk/iothub/service/models/interface.py deleted file mode 100644 index 4ea78c5c0..000000000 --- a/azext_iot/sdk/iothub/service/models/interface.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Interface(Model): - """Interface. - - :param name: Full name of digital twin interface. - :type name: str - :param properties: List of all properties in an interface. - :type properties: dict[str, ~service.models.Property] - """ - - _attribute_map = { - 'name': {'key': 'name', 'type': 'str'}, - 'properties': {'key': 'properties', 'type': '{Property}'}, - } - - def __init__(self, **kwargs): - super(Interface, self).__init__(**kwargs) - self.name = kwargs.get('name', None) - self.properties = kwargs.get('properties', None) diff --git a/azext_iot/sdk/iothub/service/models/interface_py3.py b/azext_iot/sdk/iothub/service/models/interface_py3.py deleted file mode 100644 index d89cfab2b..000000000 --- a/azext_iot/sdk/iothub/service/models/interface_py3.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Interface(Model): - """Interface. - - :param name: Full name of digital twin interface. - :type name: str - :param properties: List of all properties in an interface. - :type properties: dict[str, ~service.models.Property] - """ - - _attribute_map = { - 'name': {'key': 'name', 'type': 'str'}, - 'properties': {'key': 'properties', 'type': '{Property}'}, - } - - def __init__(self, *, name: str=None, properties=None, **kwargs) -> None: - super(Interface, self).__init__(**kwargs) - self.name = name - self.properties = properties diff --git a/azext_iot/sdk/iothub/service/models/job_properties.py b/azext_iot/sdk/iothub/service/models/job_properties.py index eab9a867e..1b52613a2 100644 --- a/azext_iot/sdk/iothub/service/models/job_properties.py +++ b/azext_iot/sdk/iothub/service/models/job_properties.py @@ -15,47 +15,58 @@ class JobProperties(Model): """JobProperties. - :param job_id: System generated. Ignored at creation. + :param job_id: The unique identifier of the job. :type job_id: str - :param start_time_utc: System generated. Ignored at creation. + :param start_time_utc: System generated. Ignored at creation. The start + date and time of the job in UTC. :type start_time_utc: datetime - :param end_time_utc: System generated. Ignored at creation. - Represents the time the job stopped processing. + :param end_time_utc: System generated. Ignored at creation. The end date + and time of the job in UTC. :type end_time_utc: datetime - :param type: Required. - The type of job to execute. Possible values include: 'unknown', 'export', + :param type: The job type. Possible values include: 'unknown', 'export', 'import', 'backup', 'readDeviceProperties', 'writeDeviceProperties', 'updateDeviceConfiguration', 'rebootDevice', 'factoryResetDevice', 'firmwareUpdate', 'scheduleDeviceMethod', 'scheduleUpdateTwin', 'restoreFromBackup', 'failoverDataCopy' :type type: str or ~service.models.enum - :param status: System generated. Ignored at creation. Possible values - include: 'unknown', 'enqueued', 'running', 'completed', 'failed', - 'cancelled', 'scheduled', 'queued' + :param status: System generated. Ignored at creation. The status of the + job. Possible values include: 'unknown', 'enqueued', 'running', + 'completed', 'failed', 'cancelled', 'scheduled', 'queued' :type status: str or ~service.models.enum - :param progress: System generated. Ignored at creation. - Represents the percentage of completion. + :param progress: System generated. Ignored at creation. The percentage of + job completion. :type progress: int - :param input_blob_container_uri: URI containing SAS token to a blob + :param input_blob_container_uri: The URI containing SAS token to a blob container that contains registry data to sync. :type input_blob_container_uri: str - :param input_blob_name: The blob name to be used when importing from the - provided input blob container. + :param input_blob_name: The blob name to use when importing from the input + blob container. :type input_blob_name: str - :param output_blob_container_uri: URI containing SAS token to a blob - container. This is used to output the status of the job and the results. + :param output_blob_container_uri: The SAS token to access the blob + container. This is used to output the status and results of the job. :type output_blob_container_uri: str - :param output_blob_name: The name of the blob that will be created in the - provided output blob container. This blob will contain - the exported device registry information for the IoT Hub. + :param output_blob_name: The blob name that will be created in the output + blob container. This blob will contain the exported device registry + information for the IoT Hub. :type output_blob_name: str :param exclude_keys_in_export: Optional for export jobs; ignored for other - jobs. Default: false. If false, authorization keys are included - in export output. Keys are exported as null otherwise. + jobs. If not specified, the service defaults to false. If false, + authorization keys are included in export output. Keys are exported as + null otherwise. :type exclude_keys_in_export: bool - :param failure_reason: System genereated. Ignored at creation. - If status == failure, this represents a string containing the reason. + :param storage_authentication_type: The authentication type used for + connecting to the storage account. Possible values include: 'keyBased', + 'identityBased' + :type storage_authentication_type: str or ~service.models.enum + :param failure_reason: System genereated. Ignored at creation. The reason + for failure, if a failure occurred. :type failure_reason: str + :param include_configurations: Defaults to false. If true, then + configurations are included in the data export/import. + :type include_configurations: bool + :param configurations_blob_name: Defaults to configurations.txt. Specifies + the name of the blob to use when exporting/importing configurations. + :type configurations_blob_name: str """ _attribute_map = { @@ -70,7 +81,10 @@ class JobProperties(Model): 'output_blob_container_uri': {'key': 'outputBlobContainerUri', 'type': 'str'}, 'output_blob_name': {'key': 'outputBlobName', 'type': 'str'}, 'exclude_keys_in_export': {'key': 'excludeKeysInExport', 'type': 'bool'}, + 'storage_authentication_type': {'key': 'storageAuthenticationType', 'type': 'str'}, 'failure_reason': {'key': 'failureReason', 'type': 'str'}, + 'include_configurations': {'key': 'includeConfigurations', 'type': 'bool'}, + 'configurations_blob_name': {'key': 'configurationsBlobName', 'type': 'str'}, } def __init__(self, **kwargs): @@ -86,4 +100,7 @@ def __init__(self, **kwargs): self.output_blob_container_uri = kwargs.get('output_blob_container_uri', None) self.output_blob_name = kwargs.get('output_blob_name', None) self.exclude_keys_in_export = kwargs.get('exclude_keys_in_export', None) + self.storage_authentication_type = kwargs.get('storage_authentication_type', None) self.failure_reason = kwargs.get('failure_reason', None) + self.include_configurations = kwargs.get('include_configurations', None) + self.configurations_blob_name = kwargs.get('configurations_blob_name', None) diff --git a/azext_iot/sdk/iothub/service/models/job_properties_py3.py b/azext_iot/sdk/iothub/service/models/job_properties_py3.py index 5712df5db..f944158ad 100644 --- a/azext_iot/sdk/iothub/service/models/job_properties_py3.py +++ b/azext_iot/sdk/iothub/service/models/job_properties_py3.py @@ -15,47 +15,58 @@ class JobProperties(Model): """JobProperties. - :param job_id: System generated. Ignored at creation. + :param job_id: The unique identifier of the job. :type job_id: str - :param start_time_utc: System generated. Ignored at creation. + :param start_time_utc: System generated. Ignored at creation. The start + date and time of the job in UTC. :type start_time_utc: datetime - :param end_time_utc: System generated. Ignored at creation. - Represents the time the job stopped processing. + :param end_time_utc: System generated. Ignored at creation. The end date + and time of the job in UTC. :type end_time_utc: datetime - :param type: Required. - The type of job to execute. Possible values include: 'unknown', 'export', + :param type: The job type. Possible values include: 'unknown', 'export', 'import', 'backup', 'readDeviceProperties', 'writeDeviceProperties', 'updateDeviceConfiguration', 'rebootDevice', 'factoryResetDevice', 'firmwareUpdate', 'scheduleDeviceMethod', 'scheduleUpdateTwin', 'restoreFromBackup', 'failoverDataCopy' :type type: str or ~service.models.enum - :param status: System generated. Ignored at creation. Possible values - include: 'unknown', 'enqueued', 'running', 'completed', 'failed', - 'cancelled', 'scheduled', 'queued' + :param status: System generated. Ignored at creation. The status of the + job. Possible values include: 'unknown', 'enqueued', 'running', + 'completed', 'failed', 'cancelled', 'scheduled', 'queued' :type status: str or ~service.models.enum - :param progress: System generated. Ignored at creation. - Represents the percentage of completion. + :param progress: System generated. Ignored at creation. The percentage of + job completion. :type progress: int - :param input_blob_container_uri: URI containing SAS token to a blob + :param input_blob_container_uri: The URI containing SAS token to a blob container that contains registry data to sync. :type input_blob_container_uri: str - :param input_blob_name: The blob name to be used when importing from the - provided input blob container. + :param input_blob_name: The blob name to use when importing from the input + blob container. :type input_blob_name: str - :param output_blob_container_uri: URI containing SAS token to a blob - container. This is used to output the status of the job and the results. + :param output_blob_container_uri: The SAS token to access the blob + container. This is used to output the status and results of the job. :type output_blob_container_uri: str - :param output_blob_name: The name of the blob that will be created in the - provided output blob container. This blob will contain - the exported device registry information for the IoT Hub. + :param output_blob_name: The blob name that will be created in the output + blob container. This blob will contain the exported device registry + information for the IoT Hub. :type output_blob_name: str :param exclude_keys_in_export: Optional for export jobs; ignored for other - jobs. Default: false. If false, authorization keys are included - in export output. Keys are exported as null otherwise. + jobs. If not specified, the service defaults to false. If false, + authorization keys are included in export output. Keys are exported as + null otherwise. :type exclude_keys_in_export: bool - :param failure_reason: System genereated. Ignored at creation. - If status == failure, this represents a string containing the reason. + :param storage_authentication_type: The authentication type used for + connecting to the storage account. Possible values include: 'keyBased', + 'identityBased' + :type storage_authentication_type: str or ~service.models.enum + :param failure_reason: System genereated. Ignored at creation. The reason + for failure, if a failure occurred. :type failure_reason: str + :param include_configurations: Defaults to false. If true, then + configurations are included in the data export/import. + :type include_configurations: bool + :param configurations_blob_name: Defaults to configurations.txt. Specifies + the name of the blob to use when exporting/importing configurations. + :type configurations_blob_name: str """ _attribute_map = { @@ -70,10 +81,13 @@ class JobProperties(Model): 'output_blob_container_uri': {'key': 'outputBlobContainerUri', 'type': 'str'}, 'output_blob_name': {'key': 'outputBlobName', 'type': 'str'}, 'exclude_keys_in_export': {'key': 'excludeKeysInExport', 'type': 'bool'}, + 'storage_authentication_type': {'key': 'storageAuthenticationType', 'type': 'str'}, 'failure_reason': {'key': 'failureReason', 'type': 'str'}, + 'include_configurations': {'key': 'includeConfigurations', 'type': 'bool'}, + 'configurations_blob_name': {'key': 'configurationsBlobName', 'type': 'str'}, } - def __init__(self, *, job_id: str=None, start_time_utc=None, end_time_utc=None, type=None, status=None, progress: int=None, input_blob_container_uri: str=None, input_blob_name: str=None, output_blob_container_uri: str=None, output_blob_name: str=None, exclude_keys_in_export: bool=None, failure_reason: str=None, **kwargs) -> None: + def __init__(self, *, job_id: str=None, start_time_utc=None, end_time_utc=None, type=None, status=None, progress: int=None, input_blob_container_uri: str=None, input_blob_name: str=None, output_blob_container_uri: str=None, output_blob_name: str=None, exclude_keys_in_export: bool=None, storage_authentication_type=None, failure_reason: str=None, include_configurations: bool=None, configurations_blob_name: str=None, **kwargs) -> None: super(JobProperties, self).__init__(**kwargs) self.job_id = job_id self.start_time_utc = start_time_utc @@ -86,4 +100,7 @@ def __init__(self, *, job_id: str=None, start_time_utc=None, end_time_utc=None, self.output_blob_container_uri = output_blob_container_uri self.output_blob_name = output_blob_name self.exclude_keys_in_export = exclude_keys_in_export + self.storage_authentication_type = storage_authentication_type self.failure_reason = failure_reason + self.include_configurations = include_configurations + self.configurations_blob_name = configurations_blob_name diff --git a/azext_iot/sdk/iothub/service/models/job_request.py b/azext_iot/sdk/iothub/service/models/job_request.py index 022d26c2b..990db418c 100644 --- a/azext_iot/sdk/iothub/service/models/job_request.py +++ b/azext_iot/sdk/iothub/service/models/job_request.py @@ -15,37 +15,35 @@ class JobRequest(Model): """JobRequest. - :param job_id: Job identifier + :param job_id: The unique identifier of the job. :type job_id: str - :param type: Required. - The type of job to execute. Possible values include: 'unknown', 'export', + :param type: The job type. Possible values include: 'unknown', 'export', 'import', 'backup', 'readDeviceProperties', 'writeDeviceProperties', 'updateDeviceConfiguration', 'rebootDevice', 'factoryResetDevice', 'firmwareUpdate', 'scheduleDeviceMethod', 'scheduleUpdateTwin', 'restoreFromBackup', 'failoverDataCopy' :type type: str or ~service.models.enum - :param cloud_to_device_method: Required if jobType is cloudToDeviceMethod. - The method type and parameters. + :param cloud_to_device_method: The method type and parameters. This is + required if the job type is cloudToDeviceMethod. :type cloud_to_device_method: ~service.models.CloudToDeviceMethod :param update_twin: :type update_twin: ~service.models.Twin - :param query_condition: Required if jobType is updateTwin or - cloudToDeviceMethod. - Condition for device query to get devices to execute the job on + :param query_condition: The condition for devices to execute the job. This + is required if the job type is updateTwin or cloudToDeviceMethod. :type query_condition: str - :param start_time: ISO 8601 date time to start the job + :param start_time: The start date and time of the job in ISO 8601 + standard. :type start_time: datetime - :param max_execution_time_in_seconds: Max execution time in secounds (ttl - duration) + :param max_execution_time_in_seconds: The maximum execution time in + secounds. :type max_execution_time_in_seconds: long """ - # @digimaun - altered update_twin type from Twin to {object} _attribute_map = { 'job_id': {'key': 'jobId', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, 'cloud_to_device_method': {'key': 'cloudToDeviceMethod', 'type': 'CloudToDeviceMethod'}, - 'update_twin': {'key': 'updateTwin', 'type': '{object}'}, + 'update_twin': {'key': 'updateTwin', 'type': 'Twin'}, 'query_condition': {'key': 'queryCondition', 'type': 'str'}, 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, 'max_execution_time_in_seconds': {'key': 'maxExecutionTimeInSeconds', 'type': 'long'}, diff --git a/azext_iot/sdk/iothub/service/models/job_request_py3.py b/azext_iot/sdk/iothub/service/models/job_request_py3.py index 3d5529711..91eadf6f0 100644 --- a/azext_iot/sdk/iothub/service/models/job_request_py3.py +++ b/azext_iot/sdk/iothub/service/models/job_request_py3.py @@ -15,37 +15,35 @@ class JobRequest(Model): """JobRequest. - :param job_id: Job identifier + :param job_id: The unique identifier of the job. :type job_id: str - :param type: Required. - The type of job to execute. Possible values include: 'unknown', 'export', + :param type: The job type. Possible values include: 'unknown', 'export', 'import', 'backup', 'readDeviceProperties', 'writeDeviceProperties', 'updateDeviceConfiguration', 'rebootDevice', 'factoryResetDevice', 'firmwareUpdate', 'scheduleDeviceMethod', 'scheduleUpdateTwin', 'restoreFromBackup', 'failoverDataCopy' :type type: str or ~service.models.enum - :param cloud_to_device_method: Required if jobType is cloudToDeviceMethod. - The method type and parameters. + :param cloud_to_device_method: The method type and parameters. This is + required if the job type is cloudToDeviceMethod. :type cloud_to_device_method: ~service.models.CloudToDeviceMethod :param update_twin: :type update_twin: ~service.models.Twin - :param query_condition: Required if jobType is updateTwin or - cloudToDeviceMethod. - Condition for device query to get devices to execute the job on + :param query_condition: The condition for devices to execute the job. This + is required if the job type is updateTwin or cloudToDeviceMethod. :type query_condition: str - :param start_time: ISO 8601 date time to start the job + :param start_time: The start date and time of the job in ISO 8601 + standard. :type start_time: datetime - :param max_execution_time_in_seconds: Max execution time in secounds (ttl - duration) + :param max_execution_time_in_seconds: The maximum execution time in + secounds. :type max_execution_time_in_seconds: long """ - # @digimaun - altered update_twin type from Twin to {object} _attribute_map = { 'job_id': {'key': 'jobId', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, 'cloud_to_device_method': {'key': 'cloudToDeviceMethod', 'type': 'CloudToDeviceMethod'}, - 'update_twin': {'key': 'updateTwin', 'type': '{object}'}, + 'update_twin': {'key': 'updateTwin', 'type': 'Twin'}, 'query_condition': {'key': 'queryCondition', 'type': 'str'}, 'start_time': {'key': 'startTime', 'type': 'iso-8601'}, 'max_execution_time_in_seconds': {'key': 'maxExecutionTimeInSeconds', 'type': 'long'}, diff --git a/azext_iot/sdk/iothub/service/models/job_response.py b/azext_iot/sdk/iothub/service/models/job_response.py index b149ae290..f9fa79858 100644 --- a/azext_iot/sdk/iothub/service/models/job_response.py +++ b/azext_iot/sdk/iothub/service/models/job_response.py @@ -15,42 +15,42 @@ class JobResponse(Model): """JobResponse. - :param job_id: System generated. Ignored at creation. + :param job_id: System generated. Ignored at creation. The unique + identifier of the job. :type job_id: str - :param query_condition: Device query condition. + :param query_condition: The device query condition. :type query_condition: str - :param created_time: System generated. Ignored at creation. + :param created_time: System generated. Ignored at creation. The creation + date and time of the job. :type created_time: datetime - :param start_time: Scheduled job start time in UTC. + :param start_time: The start date and time of the scheduled job in UTC. :type start_time: datetime - :param end_time: System generated. Ignored at creation. - Represents the time the job stopped processing. + :param end_time: System generated. Ignored at creation. The end date and + time of the job in UTC. :type end_time: datetime - :param max_execution_time_in_seconds: Max execution time in secounds (ttl - duration) + :param max_execution_time_in_seconds: The maximum execution time in + secounds. :type max_execution_time_in_seconds: long - :param type: Required. - The type of job to execute. Possible values include: 'unknown', 'export', + :param type: The job type. Possible values include: 'unknown', 'export', 'import', 'backup', 'readDeviceProperties', 'writeDeviceProperties', 'updateDeviceConfiguration', 'rebootDevice', 'factoryResetDevice', 'firmwareUpdate', 'scheduleDeviceMethod', 'scheduleUpdateTwin', 'restoreFromBackup', 'failoverDataCopy' :type type: str or ~service.models.enum - :param cloud_to_device_method: Required if jobType is cloudToDeviceMethod. - The method type and parameters. + :param cloud_to_device_method: The method type and parameters. This is + required if job type is cloudToDeviceMethod. :type cloud_to_device_method: ~service.models.CloudToDeviceMethod :param update_twin: :type update_twin: ~service.models.Twin - :param status: System generated. Ignored at creation. Possible values - include: 'unknown', 'enqueued', 'running', 'completed', 'failed', - 'cancelled', 'scheduled', 'queued' + :param status: System generated. Ignored at creation. The status of the + job. Possible values include: 'unknown', 'enqueued', 'running', + 'completed', 'failed', 'cancelled', 'scheduled', 'queued' :type status: str or ~service.models.enum - :param failure_reason: System generated. Ignored at creation. - If status == failure, this represents a string containing the reason. + :param failure_reason: The reason for the failure, if a failure occurred. :type failure_reason: str - :param status_message: Status message for the job + :param status_message: The status message of the job. :type status_message: str - :param device_job_statistics: Job details + :param device_job_statistics: The details regarding job execution status. :type device_job_statistics: ~service.models.DeviceJobStatistics """ diff --git a/azext_iot/sdk/iothub/service/models/job_response_py3.py b/azext_iot/sdk/iothub/service/models/job_response_py3.py index 55571106a..365b18f6d 100644 --- a/azext_iot/sdk/iothub/service/models/job_response_py3.py +++ b/azext_iot/sdk/iothub/service/models/job_response_py3.py @@ -15,42 +15,42 @@ class JobResponse(Model): """JobResponse. - :param job_id: System generated. Ignored at creation. + :param job_id: System generated. Ignored at creation. The unique + identifier of the job. :type job_id: str - :param query_condition: Device query condition. + :param query_condition: The device query condition. :type query_condition: str - :param created_time: System generated. Ignored at creation. + :param created_time: System generated. Ignored at creation. The creation + date and time of the job. :type created_time: datetime - :param start_time: Scheduled job start time in UTC. + :param start_time: The start date and time of the scheduled job in UTC. :type start_time: datetime - :param end_time: System generated. Ignored at creation. - Represents the time the job stopped processing. + :param end_time: System generated. Ignored at creation. The end date and + time of the job in UTC. :type end_time: datetime - :param max_execution_time_in_seconds: Max execution time in secounds (ttl - duration) + :param max_execution_time_in_seconds: The maximum execution time in + secounds. :type max_execution_time_in_seconds: long - :param type: Required. - The type of job to execute. Possible values include: 'unknown', 'export', + :param type: The job type. Possible values include: 'unknown', 'export', 'import', 'backup', 'readDeviceProperties', 'writeDeviceProperties', 'updateDeviceConfiguration', 'rebootDevice', 'factoryResetDevice', 'firmwareUpdate', 'scheduleDeviceMethod', 'scheduleUpdateTwin', 'restoreFromBackup', 'failoverDataCopy' :type type: str or ~service.models.enum - :param cloud_to_device_method: Required if jobType is cloudToDeviceMethod. - The method type and parameters. + :param cloud_to_device_method: The method type and parameters. This is + required if job type is cloudToDeviceMethod. :type cloud_to_device_method: ~service.models.CloudToDeviceMethod :param update_twin: :type update_twin: ~service.models.Twin - :param status: System generated. Ignored at creation. Possible values - include: 'unknown', 'enqueued', 'running', 'completed', 'failed', - 'cancelled', 'scheduled', 'queued' + :param status: System generated. Ignored at creation. The status of the + job. Possible values include: 'unknown', 'enqueued', 'running', + 'completed', 'failed', 'cancelled', 'scheduled', 'queued' :type status: str or ~service.models.enum - :param failure_reason: System generated. Ignored at creation. - If status == failure, this represents a string containing the reason. + :param failure_reason: The reason for the failure, if a failure occurred. :type failure_reason: str - :param status_message: Status message for the job + :param status_message: The status message of the job. :type status_message: str - :param device_job_statistics: Job details + :param device_job_statistics: The details regarding job execution status. :type device_job_statistics: ~service.models.DeviceJobStatistics """ diff --git a/azext_iot/sdk/iothub/service/models/module.py b/azext_iot/sdk/iothub/service/models/module.py index 182a9f6f9..7a40a2a04 100644 --- a/azext_iot/sdk/iothub/service/models/module.py +++ b/azext_iot/sdk/iothub/service/models/module.py @@ -13,28 +13,36 @@ class Module(Model): - """Module identity on a device. + """The module identity on a device. - :param module_id: + :param module_id: The unique identifier of the module. :type module_id: str - :param managed_by: + :param managed_by: Identifies who manages this module. For instance, this + value is \\"IotEdge\\" if the edge runtime owns this module. :type managed_by: str - :param device_id: + :param device_id: The unique identifier of the device. :type device_id: str - :param generation_id: + :param generation_id: The IoT Hub generated, case-sensitive string up to + 128 characters long. This value is used to distinguish modules with the + same moduleId, when they have been deleted and re-created. :type generation_id: str - :param etag: + :param etag: The string representing a weak ETag for the module identity, + as per RFC7232. :type etag: str - :param connection_state: Possible values include: 'Disconnected', - 'Connected' + :param connection_state: The connection state of the device. Possible + values include: 'Disconnected', 'Connected' :type connection_state: str or ~service.models.enum - :param connection_state_updated_time: + :param connection_state_updated_time: The date and time the connection + state was last updated. :type connection_state_updated_time: datetime - :param last_activity_time: + :param last_activity_time: The date and time the device last connected, + received, or sent a message. :type last_activity_time: datetime - :param cloud_to_device_message_count: + :param cloud_to_device_message_count: The number of cloud-to-module + messages currently queued to be sent to the module. :type cloud_to_device_message_count: int - :param authentication: + :param authentication: The authentication mechanism used by the module + when connecting to the service and edge hub. :type authentication: ~service.models.AuthenticationMechanism """ diff --git a/azext_iot/sdk/iothub/service/models/module_py3.py b/azext_iot/sdk/iothub/service/models/module_py3.py index 33a4e93ae..2c766ad99 100644 --- a/azext_iot/sdk/iothub/service/models/module_py3.py +++ b/azext_iot/sdk/iothub/service/models/module_py3.py @@ -13,28 +13,36 @@ class Module(Model): - """Module identity on a device. + """The module identity on a device. - :param module_id: + :param module_id: The unique identifier of the module. :type module_id: str - :param managed_by: + :param managed_by: Identifies who manages this module. For instance, this + value is \\"IotEdge\\" if the edge runtime owns this module. :type managed_by: str - :param device_id: + :param device_id: The unique identifier of the device. :type device_id: str - :param generation_id: + :param generation_id: The IoT Hub generated, case-sensitive string up to + 128 characters long. This value is used to distinguish modules with the + same moduleId, when they have been deleted and re-created. :type generation_id: str - :param etag: + :param etag: The string representing a weak ETag for the module identity, + as per RFC7232. :type etag: str - :param connection_state: Possible values include: 'Disconnected', - 'Connected' + :param connection_state: The connection state of the device. Possible + values include: 'Disconnected', 'Connected' :type connection_state: str or ~service.models.enum - :param connection_state_updated_time: + :param connection_state_updated_time: The date and time the connection + state was last updated. :type connection_state_updated_time: datetime - :param last_activity_time: + :param last_activity_time: The date and time the device last connected, + received, or sent a message. :type last_activity_time: datetime - :param cloud_to_device_message_count: + :param cloud_to_device_message_count: The number of cloud-to-module + messages currently queued to be sent to the module. :type cloud_to_device_message_count: int - :param authentication: + :param authentication: The authentication mechanism used by the module + when connecting to the service and edge hub. :type authentication: ~service.models.AuthenticationMechanism """ diff --git a/azext_iot/sdk/iothub/service/models/property.py b/azext_iot/sdk/iothub/service/models/property.py deleted file mode 100644 index 7ad081cf2..000000000 --- a/azext_iot/sdk/iothub/service/models/property.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Property(Model): - """Property. - - :param reported: - :type reported: ~service.models.Reported - :param desired: - :type desired: ~service.models.Desired - """ - - _attribute_map = { - 'reported': {'key': 'reported', 'type': 'Reported'}, - 'desired': {'key': 'desired', 'type': 'Desired'}, - } - - def __init__(self, **kwargs): - super(Property, self).__init__(**kwargs) - self.reported = kwargs.get('reported', None) - self.desired = kwargs.get('desired', None) diff --git a/azext_iot/sdk/iothub/service/models/property_container.py b/azext_iot/sdk/iothub/service/models/property_container.py index 345c535f6..ef0857dfa 100644 --- a/azext_iot/sdk/iothub/service/models/property_container.py +++ b/azext_iot/sdk/iothub/service/models/property_container.py @@ -13,18 +13,20 @@ class PropertyContainer(Model): - """Represents Twin properties. + """The desired and reported properties of the twin. The maximum depth of the + object is 10. - :param desired: Used in conjunction with reported properties to - synchronize device configuration or condition. Desired properties can only - be set by the solution back end and can be read by the device app. The - device app can also be notified in real time of changes on the desired - properties. + :param desired: The collection of desired property key-value pairs. The + keys are UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed + characters exclude UNICODE control characters (segments C0 and C1), '.', + '$' and space. The desired porperty values are JSON objects, up-to 4KB in + length. :type desired: dict[str, object] - :param reported: Used in conjunction with desired properties to - synchronize device configuration or condition. Reported properties can - only be set by the device app and can be read and queried by the solution - back end. + :param reported: The collection of reported property key-value pairs. The + keys are UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed + characters exclude UNICODE control characters (segments C0 and C1), '.', + '$' and space. The reported property values are JSON objects, up-to 4KB in + length. :type reported: dict[str, object] """ diff --git a/azext_iot/sdk/iothub/service/models/property_container_py3.py b/azext_iot/sdk/iothub/service/models/property_container_py3.py index 433224762..6ebf6ba35 100644 --- a/azext_iot/sdk/iothub/service/models/property_container_py3.py +++ b/azext_iot/sdk/iothub/service/models/property_container_py3.py @@ -13,18 +13,20 @@ class PropertyContainer(Model): - """Represents Twin properties. + """The desired and reported properties of the twin. The maximum depth of the + object is 10. - :param desired: Used in conjunction with reported properties to - synchronize device configuration or condition. Desired properties can only - be set by the solution back end and can be read by the device app. The - device app can also be notified in real time of changes on the desired - properties. + :param desired: The collection of desired property key-value pairs. The + keys are UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed + characters exclude UNICODE control characters (segments C0 and C1), '.', + '$' and space. The desired porperty values are JSON objects, up-to 4KB in + length. :type desired: dict[str, object] - :param reported: Used in conjunction with desired properties to - synchronize device configuration or condition. Reported properties can - only be set by the device app and can be read and queried by the solution - back end. + :param reported: The collection of reported property key-value pairs. The + keys are UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed + characters exclude UNICODE control characters (segments C0 and C1), '.', + '$' and space. The reported property values are JSON objects, up-to 4KB in + length. :type reported: dict[str, object] """ diff --git a/azext_iot/sdk/iothub/service/models/property_py3.py b/azext_iot/sdk/iothub/service/models/property_py3.py deleted file mode 100644 index 52d2c0846..000000000 --- a/azext_iot/sdk/iothub/service/models/property_py3.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Property(Model): - """Property. - - :param reported: - :type reported: ~service.models.Reported - :param desired: - :type desired: ~service.models.Desired - """ - - _attribute_map = { - 'reported': {'key': 'reported', 'type': 'Reported'}, - 'desired': {'key': 'desired', 'type': 'Desired'}, - } - - def __init__(self, *, reported=None, desired=None, **kwargs) -> None: - super(Property, self).__init__(**kwargs) - self.reported = reported - self.desired = desired diff --git a/azext_iot/sdk/iothub/service/models/purge_message_queue_result.py b/azext_iot/sdk/iothub/service/models/purge_message_queue_result.py index 03a91f47a..08c75b9a4 100644 --- a/azext_iot/sdk/iothub/service/models/purge_message_queue_result.py +++ b/azext_iot/sdk/iothub/service/models/purge_message_queue_result.py @@ -13,13 +13,13 @@ class PurgeMessageQueueResult(Model): - """Result of a device message queue purge operation. + """The result of a device message queue purge operation. :param total_messages_purged: :type total_messages_purged: int - :param device_id: The ID of the device whose messages are being purged. + :param device_id: The unique identifier of the device. :type device_id: str - :param module_id: The ID of the device whose messages are being purged. + :param module_id: The unique identifier of the module. :type module_id: str """ diff --git a/azext_iot/sdk/iothub/service/models/purge_message_queue_result_py3.py b/azext_iot/sdk/iothub/service/models/purge_message_queue_result_py3.py index 66454f505..d46cddfeb 100644 --- a/azext_iot/sdk/iothub/service/models/purge_message_queue_result_py3.py +++ b/azext_iot/sdk/iothub/service/models/purge_message_queue_result_py3.py @@ -13,13 +13,13 @@ class PurgeMessageQueueResult(Model): - """Result of a device message queue purge operation. + """The result of a device message queue purge operation. :param total_messages_purged: :type total_messages_purged: int - :param device_id: The ID of the device whose messages are being purged. + :param device_id: The unique identifier of the device. :type device_id: str - :param module_id: The ID of the device whose messages are being purged. + :param module_id: The unique identifier of the module. :type module_id: str """ diff --git a/azext_iot/sdk/iothub/service/models/query_result.py b/azext_iot/sdk/iothub/service/models/query_result.py index 0dcf7da64..2fe0365d3 100644 --- a/azext_iot/sdk/iothub/service/models/query_result.py +++ b/azext_iot/sdk/iothub/service/models/query_result.py @@ -21,7 +21,7 @@ class QueryResult(Model): :type type: str or ~service.models.enum :param items: The query result items, as a collection. :type items: list[object] - :param continuation_token: Request continuation token. + :param continuation_token: The continuation token. :type continuation_token: str """ diff --git a/azext_iot/sdk/iothub/service/models/query_result_py3.py b/azext_iot/sdk/iothub/service/models/query_result_py3.py index 1e0a59f1f..ed93c2c0a 100644 --- a/azext_iot/sdk/iothub/service/models/query_result_py3.py +++ b/azext_iot/sdk/iothub/service/models/query_result_py3.py @@ -21,7 +21,7 @@ class QueryResult(Model): :type type: str or ~service.models.enum :param items: The query result items, as a collection. :type items: list[object] - :param continuation_token: Request continuation token. + :param continuation_token: The continuation token. :type continuation_token: str """ diff --git a/azext_iot/sdk/iothub/service/models/query_specification.py b/azext_iot/sdk/iothub/service/models/query_specification.py index d17a6c7f3..937c84ce9 100644 --- a/azext_iot/sdk/iothub/service/models/query_specification.py +++ b/azext_iot/sdk/iothub/service/models/query_specification.py @@ -13,9 +13,9 @@ class QuerySpecification(Model): - """A Json query request. + """The Json query request. - :param query: The query. + :param query: The query string. :type query: str """ diff --git a/azext_iot/sdk/iothub/service/models/query_specification_py3.py b/azext_iot/sdk/iothub/service/models/query_specification_py3.py index 08ee4d3eb..a2ac3a4eb 100644 --- a/azext_iot/sdk/iothub/service/models/query_specification_py3.py +++ b/azext_iot/sdk/iothub/service/models/query_specification_py3.py @@ -13,9 +13,9 @@ class QuerySpecification(Model): - """A Json query request. + """The Json query request. - :param query: The query. + :param query: The query string. :type query: str """ diff --git a/azext_iot/sdk/iothub/service/models/registry_statistics.py b/azext_iot/sdk/iothub/service/models/registry_statistics.py index 2ff4f1ae0..8b072bf0b 100644 --- a/azext_iot/sdk/iothub/service/models/registry_statistics.py +++ b/azext_iot/sdk/iothub/service/models/registry_statistics.py @@ -15,11 +15,12 @@ class RegistryStatistics(Model): """RegistryStatistics. - :param total_device_count: + :param total_device_count: The total number of devices registered for the + IoT Hub. :type total_device_count: long - :param enabled_device_count: + :param enabled_device_count: The number of currently enabled devices. :type enabled_device_count: long - :param disabled_device_count: + :param disabled_device_count: The number of currently disabled devices. :type disabled_device_count: long """ diff --git a/azext_iot/sdk/iothub/service/models/registry_statistics_py3.py b/azext_iot/sdk/iothub/service/models/registry_statistics_py3.py index be72cac00..ca24e948c 100644 --- a/azext_iot/sdk/iothub/service/models/registry_statistics_py3.py +++ b/azext_iot/sdk/iothub/service/models/registry_statistics_py3.py @@ -15,11 +15,12 @@ class RegistryStatistics(Model): """RegistryStatistics. - :param total_device_count: + :param total_device_count: The total number of devices registered for the + IoT Hub. :type total_device_count: long - :param enabled_device_count: + :param enabled_device_count: The number of currently enabled devices. :type enabled_device_count: long - :param disabled_device_count: + :param disabled_device_count: The number of currently disabled devices. :type disabled_device_count: long """ diff --git a/azext_iot/sdk/iothub/service/models/reported.py b/azext_iot/sdk/iothub/service/models/reported.py deleted file mode 100644 index 702c81416..000000000 --- a/azext_iot/sdk/iothub/service/models/reported.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Reported(Model): - """Reported. - - :param value: The current interface property value in a digitalTwin. - :type value: object - :param desired_state: - :type desired_state: ~service.models.DesiredState - """ - - _attribute_map = { - 'value': {'key': 'value', 'type': 'object'}, - 'desired_state': {'key': 'desiredState', 'type': 'DesiredState'}, - } - - def __init__(self, **kwargs): - super(Reported, self).__init__(**kwargs) - self.value = kwargs.get('value', None) - self.desired_state = kwargs.get('desired_state', None) diff --git a/azext_iot/sdk/iothub/service/models/reported_py3.py b/azext_iot/sdk/iothub/service/models/reported_py3.py deleted file mode 100644 index 933e34420..000000000 --- a/azext_iot/sdk/iothub/service/models/reported_py3.py +++ /dev/null @@ -1,32 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.serialization import Model - - -class Reported(Model): - """Reported. - - :param value: The current interface property value in a digitalTwin. - :type value: object - :param desired_state: - :type desired_state: ~service.models.DesiredState - """ - - _attribute_map = { - 'value': {'key': 'value', 'type': 'object'}, - 'desired_state': {'key': 'desiredState', 'type': 'DesiredState'}, - } - - def __init__(self, *, value=None, desired_state=None, **kwargs) -> None: - super(Reported, self).__init__(**kwargs) - self.value = value - self.desired_state = desired_state diff --git a/azext_iot/sdk/iothub/service/models/service_statistics.py b/azext_iot/sdk/iothub/service/models/service_statistics.py index aef69ff65..f2b6d9031 100644 --- a/azext_iot/sdk/iothub/service/models/service_statistics.py +++ b/azext_iot/sdk/iothub/service/models/service_statistics.py @@ -15,7 +15,7 @@ class ServiceStatistics(Model): """ServiceStatistics. - :param connected_device_count: + :param connected_device_count: The number of currently connected devices. :type connected_device_count: long """ diff --git a/azext_iot/sdk/iothub/service/models/service_statistics_py3.py b/azext_iot/sdk/iothub/service/models/service_statistics_py3.py index cecd023c8..360054e23 100644 --- a/azext_iot/sdk/iothub/service/models/service_statistics_py3.py +++ b/azext_iot/sdk/iothub/service/models/service_statistics_py3.py @@ -15,7 +15,7 @@ class ServiceStatistics(Model): """ServiceStatistics. - :param connected_device_count: + :param connected_device_count: The number of currently connected devices. :type connected_device_count: long """ diff --git a/azext_iot/sdk/iothub/service/models/symmetric_key.py b/azext_iot/sdk/iothub/service/models/symmetric_key.py index e1401d814..a4266245d 100644 --- a/azext_iot/sdk/iothub/service/models/symmetric_key.py +++ b/azext_iot/sdk/iothub/service/models/symmetric_key.py @@ -15,9 +15,9 @@ class SymmetricKey(Model): """SymmetricKey. - :param primary_key: + :param primary_key: The base64 encoded primary key of the device. :type primary_key: str - :param secondary_key: + :param secondary_key: The base64 encoded secondary key of the device. :type secondary_key: str """ diff --git a/azext_iot/sdk/iothub/service/models/symmetric_key_py3.py b/azext_iot/sdk/iothub/service/models/symmetric_key_py3.py index b34488d47..1fd90ab01 100644 --- a/azext_iot/sdk/iothub/service/models/symmetric_key_py3.py +++ b/azext_iot/sdk/iothub/service/models/symmetric_key_py3.py @@ -15,9 +15,9 @@ class SymmetricKey(Model): """SymmetricKey. - :param primary_key: + :param primary_key: The base64 encoded primary key of the device. :type primary_key: str - :param secondary_key: + :param secondary_key: The base64 encoded secondary key of the device. :type secondary_key: str """ diff --git a/azext_iot/sdk/iothub/service/models/twin.py b/azext_iot/sdk/iothub/service/models/twin.py index 9487c13ee..58cd9acb0 100644 --- a/azext_iot/sdk/iothub/service/models/twin.py +++ b/azext_iot/sdk/iothub/service/models/twin.py @@ -13,57 +13,74 @@ class Twin(Model): - """Twin Representation. + """The state information for a device or module. This is implicitly created + and deleted when the corresponding device/ module identity is created or + deleted in the IoT Hub. - :param device_id: The deviceId uniquely identifies the device in the IoT - hub's identity registry. A case-sensitive string (up to 128 char long) of - ASCII 7-bit alphanumeric chars + {'-', ':', '.', '+', '%', '_', '#', '*', - '?', '!', '(', ')', ',', '=', '@', ';', '$', '''}. + :param device_id: The unique identifier of the device in the identity + registry of the IoT Hub. It is a case-sensitive string (up to 128 char + long) of ASCII 7-bit alphanumeric chars, and the following special + characters {'-', ':', '.', '+', '%', '_', '#', '*', '?', '!', '(', ')', + ',', '=', '@', ';', '$', '''}. :type device_id: str - :param module_id: Gets and sets the Module Id. + :param module_id: The unique identifier of the module in the identity + registry of the IoT Hub. It is a case-sensitive string (up to 128 char + long) of ASCII 7-bit alphanumeric chars, and the following special + characters {'-', ':', '.', '+', '%', '_', '#', '*', '?', '!', '(', ')', + ',', '=', '@', ';', '$', '''}. :type module_id: str - :param tags: A JSON document read and written by the solution back end. - Tags are not visible to device apps. + :param tags: The collection of key-value pairs read and written by the + solution back end. They are not visible to device apps. They keys are + UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed characters + exclude UNICODE control characters (segments C0 and C1), '.', '$' and + space. The values are JSON objects, up-to 4KB in length. :type tags: dict[str, object] - :param properties: Gets and sets the Twin properties. + :param properties: The desired and reported properties of the twin. :type properties: ~service.models.TwinProperties - :param etag: Twin's ETag + :param etag: The string representing a ETag for the device twin, as per + RFC7232. :type etag: str - :param version: Version for device twin, including tags and desired + :param version: The version for the device twin including tags and desired properties :type version: long - :param device_etag: Device's ETag + :param device_etag: The string representing a ETag for the device, as per + RFC7232. :type device_etag: str - :param status: Gets the corresponding Device's Status. Possible values - include: 'enabled', 'disabled' + :param status: The enabled status of the device. If disabled, the device + cannot connect to the service. Possible values include: 'enabled', + 'disabled' :type status: str or ~service.models.enum - :param status_reason: Reason, if any, for the corresponding Device to be - in specified Status + :param status_reason: The reason for the current status of the device, if + any. :type status_reason: str - :param status_update_time: Time when the corresponding Device's Status was - last updated + :param status_update_time: The date and time when the status of the device + was last updated. :type status_update_time: datetime - :param connection_state: Corresponding Device's ConnectionState. Possible + :param connection_state: The connection state of the device. Possible values include: 'Disconnected', 'Connected' :type connection_state: str or ~service.models.enum - :param last_activity_time: The last time the device connected, received or - sent a message. In ISO8601 datetime format in UTC, for example, - 2015-01-28T16:24:48.789Z. This does not update if the device uses the - HTTP/1 protocol to perform messaging operations. + :param last_activity_time: The date and time when the device last + connected or received or sent a message. The date and time is sepecified + in ISO8601 datetime format in UTC, for example, 2015-01-28T16:24:48.789Z. + This value is not updated if the device uses the HTTP/1 protocol to + perform messaging operations. :type last_activity_time: datetime - :param cloud_to_device_message_count: Number of messages sent to the - corresponding Device from the Cloud + :param cloud_to_device_message_count: The number of cloud-to-device + messages sent. :type cloud_to_device_message_count: int - :param authentication_type: Corresponding Device's authentication type. + :param authentication_type: The authentication type used by the device. Possible values include: 'sas', 'selfSigned', 'certificateAuthority', 'none' :type authentication_type: str or ~service.models.enum - :param x509_thumbprint: Corresponding Device's X509 thumbprint + :param x509_thumbprint: The X509 thumbprint of the device. :type x509_thumbprint: ~service.models.X509Thumbprint :param capabilities: :type capabilities: ~service.models.DeviceCapabilities - :param device_scope: + :param device_scope: The scope of the device. :type device_scope: str + :param parent_scopes: The scopes of the upper level edge devices if + applicable. Only available for edge devices. + :type parent_scopes: list[str] """ _attribute_map = { @@ -84,6 +101,7 @@ class Twin(Model): 'x509_thumbprint': {'key': 'x509Thumbprint', 'type': 'X509Thumbprint'}, 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, 'device_scope': {'key': 'deviceScope', 'type': 'str'}, + 'parent_scopes': {'key': 'parentScopes', 'type': '[str]'}, } def __init__(self, **kwargs): @@ -105,3 +123,4 @@ def __init__(self, **kwargs): self.x509_thumbprint = kwargs.get('x509_thumbprint', None) self.capabilities = kwargs.get('capabilities', None) self.device_scope = kwargs.get('device_scope', None) + self.parent_scopes = kwargs.get('parent_scopes', None) diff --git a/azext_iot/sdk/iothub/service/models/twin_properties.py b/azext_iot/sdk/iothub/service/models/twin_properties.py index 36bf22333..b2aa76c26 100644 --- a/azext_iot/sdk/iothub/service/models/twin_properties.py +++ b/azext_iot/sdk/iothub/service/models/twin_properties.py @@ -13,18 +13,20 @@ class TwinProperties(Model): - """Represents Twin properties. + """The desired and reported properties of the twin. The maximum depth of the + object is 10. - :param desired: Used in conjunction with reported properties to - synchronize device configuration or condition. Desired properties can only - be set by the solution back end and can be read by the device app. The - device app can also be notified in real time of changes on the desired - properties. + :param desired: The collection of desired property key-value pairs. The + keys are UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed + characters exclude UNICODE control characters (segments C0 and C1), '.', + '$' and space. The desired porperty values are JSON objects, up-to 4KB in + length. :type desired: dict[str, object] - :param reported: Used in conjunction with desired properties to - synchronize device configuration or condition. Reported properties can - only be set by the device app and can be read and queried by the solution - back end. + :param reported: The collection of reported property key-value pairs. The + keys are UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed + characters exclude UNICODE control characters (segments C0 and C1), '.', + '$' and space. The reported property values are JSON objects, up-to 4KB in + length. :type reported: dict[str, object] """ diff --git a/azext_iot/sdk/iothub/service/models/twin_properties_py3.py b/azext_iot/sdk/iothub/service/models/twin_properties_py3.py index 8d7930b47..651b67394 100644 --- a/azext_iot/sdk/iothub/service/models/twin_properties_py3.py +++ b/azext_iot/sdk/iothub/service/models/twin_properties_py3.py @@ -13,18 +13,20 @@ class TwinProperties(Model): - """Represents Twin properties. + """The desired and reported properties of the twin. The maximum depth of the + object is 10. - :param desired: Used in conjunction with reported properties to - synchronize device configuration or condition. Desired properties can only - be set by the solution back end and can be read by the device app. The - device app can also be notified in real time of changes on the desired - properties. + :param desired: The collection of desired property key-value pairs. The + keys are UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed + characters exclude UNICODE control characters (segments C0 and C1), '.', + '$' and space. The desired porperty values are JSON objects, up-to 4KB in + length. :type desired: dict[str, object] - :param reported: Used in conjunction with desired properties to - synchronize device configuration or condition. Reported properties can - only be set by the device app and can be read and queried by the solution - back end. + :param reported: The collection of reported property key-value pairs. The + keys are UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed + characters exclude UNICODE control characters (segments C0 and C1), '.', + '$' and space. The reported property values are JSON objects, up-to 4KB in + length. :type reported: dict[str, object] """ diff --git a/azext_iot/sdk/iothub/service/models/twin_py3.py b/azext_iot/sdk/iothub/service/models/twin_py3.py index 701a1af5d..8328ba33a 100644 --- a/azext_iot/sdk/iothub/service/models/twin_py3.py +++ b/azext_iot/sdk/iothub/service/models/twin_py3.py @@ -13,57 +13,74 @@ class Twin(Model): - """Twin Representation. + """The state information for a device or module. This is implicitly created + and deleted when the corresponding device/ module identity is created or + deleted in the IoT Hub. - :param device_id: The deviceId uniquely identifies the device in the IoT - hub's identity registry. A case-sensitive string (up to 128 char long) of - ASCII 7-bit alphanumeric chars + {'-', ':', '.', '+', '%', '_', '#', '*', - '?', '!', '(', ')', ',', '=', '@', ';', '$', '''}. + :param device_id: The unique identifier of the device in the identity + registry of the IoT Hub. It is a case-sensitive string (up to 128 char + long) of ASCII 7-bit alphanumeric chars, and the following special + characters {'-', ':', '.', '+', '%', '_', '#', '*', '?', '!', '(', ')', + ',', '=', '@', ';', '$', '''}. :type device_id: str - :param module_id: Gets and sets the Module Id. + :param module_id: The unique identifier of the module in the identity + registry of the IoT Hub. It is a case-sensitive string (up to 128 char + long) of ASCII 7-bit alphanumeric chars, and the following special + characters {'-', ':', '.', '+', '%', '_', '#', '*', '?', '!', '(', ')', + ',', '=', '@', ';', '$', '''}. :type module_id: str - :param tags: A JSON document read and written by the solution back end. - Tags are not visible to device apps. + :param tags: The collection of key-value pairs read and written by the + solution back end. They are not visible to device apps. They keys are + UTF-8 encoded, case-sensitive and up-to 1KB in length. Allowed characters + exclude UNICODE control characters (segments C0 and C1), '.', '$' and + space. The values are JSON objects, up-to 4KB in length. :type tags: dict[str, object] - :param properties: Gets and sets the Twin properties. + :param properties: The desired and reported properties of the twin. :type properties: ~service.models.TwinProperties - :param etag: Twin's ETag + :param etag: The string representing a ETag for the device twin, as per + RFC7232. :type etag: str - :param version: Version for device twin, including tags and desired + :param version: The version for the device twin including tags and desired properties :type version: long - :param device_etag: Device's ETag + :param device_etag: The string representing a ETag for the device, as per + RFC7232. :type device_etag: str - :param status: Gets the corresponding Device's Status. Possible values - include: 'enabled', 'disabled' + :param status: The enabled status of the device. If disabled, the device + cannot connect to the service. Possible values include: 'enabled', + 'disabled' :type status: str or ~service.models.enum - :param status_reason: Reason, if any, for the corresponding Device to be - in specified Status + :param status_reason: The reason for the current status of the device, if + any. :type status_reason: str - :param status_update_time: Time when the corresponding Device's Status was - last updated + :param status_update_time: The date and time when the status of the device + was last updated. :type status_update_time: datetime - :param connection_state: Corresponding Device's ConnectionState. Possible + :param connection_state: The connection state of the device. Possible values include: 'Disconnected', 'Connected' :type connection_state: str or ~service.models.enum - :param last_activity_time: The last time the device connected, received or - sent a message. In ISO8601 datetime format in UTC, for example, - 2015-01-28T16:24:48.789Z. This does not update if the device uses the - HTTP/1 protocol to perform messaging operations. + :param last_activity_time: The date and time when the device last + connected or received or sent a message. The date and time is sepecified + in ISO8601 datetime format in UTC, for example, 2015-01-28T16:24:48.789Z. + This value is not updated if the device uses the HTTP/1 protocol to + perform messaging operations. :type last_activity_time: datetime - :param cloud_to_device_message_count: Number of messages sent to the - corresponding Device from the Cloud + :param cloud_to_device_message_count: The number of cloud-to-device + messages sent. :type cloud_to_device_message_count: int - :param authentication_type: Corresponding Device's authentication type. + :param authentication_type: The authentication type used by the device. Possible values include: 'sas', 'selfSigned', 'certificateAuthority', 'none' :type authentication_type: str or ~service.models.enum - :param x509_thumbprint: Corresponding Device's X509 thumbprint + :param x509_thumbprint: The X509 thumbprint of the device. :type x509_thumbprint: ~service.models.X509Thumbprint :param capabilities: :type capabilities: ~service.models.DeviceCapabilities - :param device_scope: + :param device_scope: The scope of the device. :type device_scope: str + :param parent_scopes: The scopes of the upper level edge devices if + applicable. Only available for edge devices. + :type parent_scopes: list[str] """ _attribute_map = { @@ -84,9 +101,10 @@ class Twin(Model): 'x509_thumbprint': {'key': 'x509Thumbprint', 'type': 'X509Thumbprint'}, 'capabilities': {'key': 'capabilities', 'type': 'DeviceCapabilities'}, 'device_scope': {'key': 'deviceScope', 'type': 'str'}, + 'parent_scopes': {'key': 'parentScopes', 'type': '[str]'}, } - def __init__(self, *, device_id: str=None, module_id: str=None, tags=None, properties=None, etag: str=None, version: int=None, device_etag: str=None, status=None, status_reason: str=None, status_update_time=None, connection_state=None, last_activity_time=None, cloud_to_device_message_count: int=None, authentication_type=None, x509_thumbprint=None, capabilities=None, device_scope: str=None, **kwargs) -> None: + def __init__(self, *, device_id: str=None, module_id: str=None, tags=None, properties=None, etag: str=None, version: int=None, device_etag: str=None, status=None, status_reason: str=None, status_update_time=None, connection_state=None, last_activity_time=None, cloud_to_device_message_count: int=None, authentication_type=None, x509_thumbprint=None, capabilities=None, device_scope: str=None, parent_scopes=None, **kwargs) -> None: super(Twin, self).__init__(**kwargs) self.device_id = device_id self.module_id = module_id @@ -105,3 +123,4 @@ def __init__(self, *, device_id: str=None, module_id: str=None, tags=None, prope self.x509_thumbprint = x509_thumbprint self.capabilities = capabilities self.device_scope = device_scope + self.parent_scopes = parent_scopes diff --git a/azext_iot/sdk/iothub/service/models/x509_thumbprint.py b/azext_iot/sdk/iothub/service/models/x509_thumbprint.py index 450f74d3c..b95bdf380 100644 --- a/azext_iot/sdk/iothub/service/models/x509_thumbprint.py +++ b/azext_iot/sdk/iothub/service/models/x509_thumbprint.py @@ -15,9 +15,10 @@ class X509Thumbprint(Model): """X509Thumbprint. - :param primary_thumbprint: + :param primary_thumbprint: The X509 client certificate primary thumbprint. :type primary_thumbprint: str - :param secondary_thumbprint: + :param secondary_thumbprint: The X509 client certificate secondary + thumbprint. :type secondary_thumbprint: str """ diff --git a/azext_iot/sdk/iothub/service/models/x509_thumbprint_py3.py b/azext_iot/sdk/iothub/service/models/x509_thumbprint_py3.py index b2c70b507..037fc70da 100644 --- a/azext_iot/sdk/iothub/service/models/x509_thumbprint_py3.py +++ b/azext_iot/sdk/iothub/service/models/x509_thumbprint_py3.py @@ -15,9 +15,10 @@ class X509Thumbprint(Model): """X509Thumbprint. - :param primary_thumbprint: + :param primary_thumbprint: The X509 client certificate primary thumbprint. :type primary_thumbprint: str - :param secondary_thumbprint: + :param secondary_thumbprint: The X509 client certificate secondary + thumbprint. :type secondary_thumbprint: str """ diff --git a/azext_iot/sdk/iothub/service/operations/__init__.py b/azext_iot/sdk/iothub/service/operations/__init__.py index 727394b4d..caaa8e08a 100644 --- a/azext_iot/sdk/iothub/service/operations/__init__.py +++ b/azext_iot/sdk/iothub/service/operations/__init__.py @@ -10,19 +10,23 @@ # -------------------------------------------------------------------------- from .configuration_operations import ConfigurationOperations -from .registry_manager_operations import RegistryManagerOperations -from .job_client_operations import JobClientOperations -from .fault_injection_operations import FaultInjectionOperations -from .twin_operations import TwinOperations +from .statistics_operations import StatisticsOperations +from .devices_operations import DevicesOperations +from .bulk_registry_operations import BulkRegistryOperations +from .query_operations import QueryOperations +from .jobs_operations import JobsOperations +from .cloud_to_device_messages_operations import CloudToDeviceMessagesOperations +from .modules_operations import ModulesOperations from .digital_twin_operations import DigitalTwinOperations -from .device_method_operations import DeviceMethodOperations __all__ = [ 'ConfigurationOperations', - 'RegistryManagerOperations', - 'JobClientOperations', - 'FaultInjectionOperations', - 'TwinOperations', + 'StatisticsOperations', + 'DevicesOperations', + 'BulkRegistryOperations', + 'QueryOperations', + 'JobsOperations', + 'CloudToDeviceMessagesOperations', + 'ModulesOperations', 'DigitalTwinOperations', - 'DeviceMethodOperations', ] diff --git a/azext_iot/sdk/iothub/service/operations/bulk_registry_operations.py b/azext_iot/sdk/iothub/service/operations/bulk_registry_operations.py new file mode 100644 index 000000000..2a25ba1d6 --- /dev/null +++ b/azext_iot/sdk/iothub/service/operations/bulk_registry_operations.py @@ -0,0 +1,104 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrestazure.azure_exceptions import CloudError + +from .. import models + + +class BulkRegistryOperations(object): + """BulkRegistryOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the Api. Constant value: "2020-09-30". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-09-30" + + self.config = config + + def update_registry( + self, devices, custom_headers=None, raw=False, **operation_config): + """Creates, updates, or deletes the identities of multiple devices from + the IoT Hub identity registry. A device identity can be specified only + once in the list. Different operations (create, update, delete) on + different devices are allowed. A maximum of 100 devices can be + specified per invocation. For large scale operations, use the import + feature using blob storage + (https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities). + + :param devices: The registry operations to perform. + :type devices: list[~service.models.ExportImportDevice] + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: BulkRegistryOperationResult or ClientRawResponse if raw=true + :rtype: ~service.models.BulkRegistryOperationResult or + ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.update_registry.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(devices, '[ExportImportDevice]') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 400]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('BulkRegistryOperationResult', response) + if response.status_code == 400: + deserialized = self._deserialize('BulkRegistryOperationResult', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + update_registry.metadata = {'url': '/devices'} diff --git a/azext_iot/sdk/iothub/service/operations/cloud_to_device_messages_operations.py b/azext_iot/sdk/iothub/service/operations/cloud_to_device_messages_operations.py new file mode 100644 index 000000000..86b598e5c --- /dev/null +++ b/azext_iot/sdk/iothub/service/operations/cloud_to_device_messages_operations.py @@ -0,0 +1,249 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrestazure.azure_exceptions import CloudError + +from .. import models + + +class CloudToDeviceMessagesOperations(object): + """CloudToDeviceMessagesOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the Api. Constant value: "2020-09-30". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-09-30" + + self.config = config + + def purge_cloud_to_device_message_queue( + self, id, custom_headers=None, raw=False, **operation_config): + """Deletes all the pending commands for a device in the IoT Hub. + + :param id: The unique identifier of the device. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: PurgeMessageQueueResult or ClientRawResponse if raw=true + :rtype: ~service.models.PurgeMessageQueueResult or + ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.purge_cloud_to_device_message_queue.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('PurgeMessageQueueResult', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + purge_cloud_to_device_message_queue.metadata = {'url': '/devices/{id}/commands'} + + def receive_feedback_notification( + self, custom_headers=None, raw=False, **operation_config): + """Gets the feedback for cloud-to-device messages. See + https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging for + more information. This capability is only available in the standard + tier IoT Hub. For more information, see [Choose the right IoT Hub + tier](https://aka.ms/scaleyouriotsolution). + + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.receive_feedback_notification.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 204]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + receive_feedback_notification.metadata = {'url': '/messages/serviceBound/feedback'} + + def complete_feedback_notification( + self, lock_token, custom_headers=None, raw=False, **operation_config): + """Completes the cloud-to-device feedback message. A completed message is + deleted from the feedback queue of the service. See + https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging for + more information. + + :param lock_token: The lock token obtained when the cloud-to-device + message is received. This is used to resolve race conditions when + completing a feedback message. + :type lock_token: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.complete_feedback_notification.metadata['url'] + path_format_arguments = { + 'lockToken': self._serialize.url("lock_token", lock_token, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + complete_feedback_notification.metadata = {'url': '/messages/serviceBound/feedback/{lockToken}'} + + def abandon_feedback_notification( + self, lock_token, custom_headers=None, raw=False, **operation_config): + """Abandons the lock on a cloud-to-device feedback message. See + https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messaging for + more information. + + :param lock_token: The lock token obtained when the cloud-to-device + message is received. + :type lock_token: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.abandon_feedback_notification.metadata['url'] + path_format_arguments = { + 'lockToken': self._serialize.url("lock_token", lock_token, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + abandon_feedback_notification.metadata = {'url': '/messages/serviceBound/feedback/{lockToken}/abandon'} diff --git a/azext_iot/sdk/iothub/service/operations/configuration_operations.py b/azext_iot/sdk/iothub/service/operations/configuration_operations.py index f49d8ca18..a1f2bf195 100644 --- a/azext_iot/sdk/iothub/service/operations/configuration_operations.py +++ b/azext_iot/sdk/iothub/service/operations/configuration_operations.py @@ -23,7 +23,7 @@ class ConfigurationOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the Api. Constant value: "2019-10-01". + :ivar api_version: Version of the Api. Constant value: "2020-09-30". """ models = models @@ -33,16 +33,16 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2019-10-01" + self.api_version = "2020-09-30" self.config = config def get( self, id, custom_headers=None, raw=False, **operation_config): - """Retrieve a configuration for Iot Hub devices and modules by it - identifier. + """Gets a configuration on the IoT Hub for automatic device/module + management. - :param id: + :param id: The unique identifier of the configuration. :type id: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -98,16 +98,17 @@ def get( def create_or_update( self, id, configuration, if_match=None, custom_headers=None, raw=False, **operation_config): - """Create or update the configuration for devices or modules of an IoT - hub. An ETag must not be specified for the create operation. An ETag - must be specified for the update operation. Note that configuration Id - and Content cannot be updated by the user. + """Creates or updates a configuration on the IoT Hub for automatic + device/module management. Configuration identifier and Content cannot + be updated. - :param id: + :param id: The unique identifier of the configuration. :type id: str - :param configuration: + :param configuration: The configuration to be created or updated. :type configuration: ~service.models.Configuration - :param if_match: + :param if_match: The string representing a weak ETag for the + configuration, as per RFC7232. This should not be set when creating a + configuration, but may be set when updating a configuration. :type if_match: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -171,19 +172,17 @@ def create_or_update( def delete( self, id, if_match=None, custom_headers=None, raw=False, **operation_config): - """Delete the configuration for devices or modules of an IoT hub. This - request requires the If-Match header. The client may specify the ETag - for the device identity on the request in order to compare to the ETag - maintained by the service for the purpose of optimistic concurrency. - The delete operation is performed only if the ETag sent by the client - matches the value maintained by the server, indicating that the device - identity has not been modified since it was retrieved by the client. To - force an unconditional delete, set If-Match to the wildcard character - (*). - - :param id: + """Deletes a configuration on the IoT Hub for automatic device/module + management. + + :param id: The unique identifier of the configuration. :type id: str - :param if_match: + :param if_match: The string representing a weak ETag for the + configuration, as per RFC7232. The delete operation is performed only + if this ETag matches the value maintained by the server, indicating + that the configuration has not been modified since it was last + retrieved. To force an unconditional delete, set If-Match to the + wildcard character (*). :type if_match: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -232,11 +231,12 @@ def delete( def get_configurations( self, top=None, custom_headers=None, raw=False, **operation_config): - """Get multiple configurations for devices or modules of an IoT Hub. - Returns the specified number of configurations for Iot Hub. Pagination - is not supported. + """Gets configurations on the IoT Hub for automatic device/module + management. Pagination is not supported. - :param top: + :param top: The number of configurations to retrieve. Value will be + overridden if greater than the maximum deployment count for the IoT + Hub. :type top: int :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -290,15 +290,14 @@ def get_configurations( def test_queries( self, target_condition=None, custom_metric_queries=None, custom_headers=None, raw=False, **operation_config): - """Validates the target condition query and custom metric queries for a - configuration. - - Validates the target condition query and custom metric queries for a - configuration. + """Validates target condition and custom metric queries for a + configuration on the IoT Hub. - :param target_condition: + :param target_condition: The query used to define targeted devices or + modules. The query is based on twin tags and/or reported properties. :type target_condition: str - :param custom_metric_queries: + :param custom_metric_queries: The key-value pairs with queries and + their identifier. :type custom_metric_queries: dict[str, str] :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -357,23 +356,19 @@ def test_queries( def apply_on_edge_device( self, id, content, custom_headers=None, raw=False, **operation_config): - """Applies the provided configuration content to the specified edge - device. - - Applies the provided configuration content to the specified edge - device. Configuration content must have modules content. + """Applies the configuration content to an edge device. - :param id: Device ID. + :param id: The unique identifier of the edge device. :type id: str - :param content: Configuration Content. + :param content: The configuration content. :type content: ~service.models.ConfigurationContent :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse :raises: :class:`CloudError` """ # Construct URL @@ -389,7 +384,6 @@ def apply_on_edge_device( # Construct headers header_parameters = {} - header_parameters['Accept'] = 'application/json' header_parameters['Content-Type'] = 'application/json; charset=utf-8' if self.config.generate_client_request_id: header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) diff --git a/azext_iot/sdk/iothub/service/operations/device_method_operations.py b/azext_iot/sdk/iothub/service/operations/device_method_operations.py deleted file mode 100644 index 74b5ff6a7..000000000 --- a/azext_iot/sdk/iothub/service/operations/device_method_operations.py +++ /dev/null @@ -1,177 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -import uuid -from msrest.pipeline import ClientRawResponse -from msrestazure.azure_exceptions import CloudError - -from .. import models - - -class DeviceMethodOperations(object): - """DeviceMethodOperations operations. - - :param client: Client for service requests. - :param config: Configuration of service client. - :param serializer: An object model serializer. - :param deserializer: An object model deserializer. - :ivar api_version: Version of the Api. Constant value: "2019-10-01". - """ - - models = models - - def __init__(self, client, config, serializer, deserializer): - - self._client = client - self._serialize = serializer - self._deserialize = deserializer - self.api_version = "2019-10-01" - - self.config = config - - def invoke_device_method( - self, device_id, direct_method_request, custom_headers=None, raw=False, **operation_config): - """Invoke a direct method on a device. - - Invoke a direct method on a device. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-direct-methods - for more information. - - :param device_id: - :type device_id: str - :param direct_method_request: - :type direct_method_request: ~service.models.CloudToDeviceMethod - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: CloudToDeviceMethodResult or ClientRawResponse if raw=true - :rtype: ~service.models.CloudToDeviceMethodResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.invoke_device_method.metadata['url'] - path_format_arguments = { - 'deviceId': self._serialize.url("device_id", device_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(direct_method_request, 'CloudToDeviceMethod') - - # Construct and send request - request = self._client.post(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('CloudToDeviceMethodResult', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - invoke_device_method.metadata = {'url': '/twins/{deviceId}/methods'} - - def invoke_module_method( - self, device_id, module_id, direct_method_request, custom_headers=None, raw=False, **operation_config): - """Invoke a direct method on a module of a device. - - Invoke a direct method on a module of a device. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-direct-methods - for more information. - - :param device_id: - :type device_id: str - :param module_id: - :type module_id: str - :param direct_method_request: - :type direct_method_request: ~service.models.CloudToDeviceMethod - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: CloudToDeviceMethodResult or ClientRawResponse if raw=true - :rtype: ~service.models.CloudToDeviceMethodResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.invoke_module_method.metadata['url'] - path_format_arguments = { - 'deviceId': self._serialize.url("device_id", device_id, 'str'), - 'moduleId': self._serialize.url("module_id", module_id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(direct_method_request, 'CloudToDeviceMethod') - - # Construct and send request - request = self._client.post(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('CloudToDeviceMethodResult', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - invoke_module_method.metadata = {'url': '/twins/{deviceId}/modules/{moduleId}/methods'} diff --git a/azext_iot/sdk/iothub/service/operations/twin_operations.py b/azext_iot/sdk/iothub/service/operations/devices_operations.py similarity index 60% rename from azext_iot/sdk/iothub/service/operations/twin_operations.py rename to azext_iot/sdk/iothub/service/operations/devices_operations.py index 532a395e5..487a90a18 100644 --- a/azext_iot/sdk/iothub/service/operations/twin_operations.py +++ b/azext_iot/sdk/iothub/service/operations/devices_operations.py @@ -16,14 +16,14 @@ from .. import models -class TwinOperations(object): - """TwinOperations operations. +class DevicesOperations(object): + """DevicesOperations operations. :param client: Client for service requests. :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the Api. Constant value: "2019-10-01". + :ivar api_version: Version of the Api. Constant value: "2020-09-30". """ models = models @@ -33,38 +33,40 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2019-10-01" + self.api_version = "2020-09-30" self.config = config - def get_device_twin( - self, id, custom_headers=None, raw=False, **operation_config): - """Gets a device twin. - - Gets a device twin. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins + def get_devices( + self, top=None, custom_headers=None, raw=False, **operation_config): + """Gets the identities of multiple devices from the IoT Hub identity + registry. Not recommended. Use the IoT Hub query API to retrieve device + twin and device identity information. See + https://docs.microsoft.com/en-us/rest/api/iothub/service/queryiothub + and + https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-query-language for more information. - :param id: Device ID. - :type id: str + :param top: The maximum number of device identities returned by the + query. Any value outside the range of 1-1000 is considered to be 1000. + :type top: int :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: Twin or ClientRawResponse if raw=true - :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse + :return: list or ClientRawResponse if raw=true + :rtype: list[~service.models.Device] or + ~msrest.pipeline.ClientRawResponse :raises: :class:`CloudError` """ # Construct URL - url = self.get_device_twin.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) + url = self.get_devices.metadata['url'] # Construct parameters query_parameters = {} + if top is not None: + query_parameters['top'] = self._serialize.query("top", top, 'int') query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') # Construct headers @@ -89,40 +91,32 @@ def get_device_twin( deserialized = None if response.status_code == 200: - deserialized = self._deserialize('Twin', response) + deserialized = self._deserialize('[Device]', response) if raw: client_raw_response = ClientRawResponse(deserialized, response) return client_raw_response return deserialized - get_device_twin.metadata = {'url': '/twins/{id}'} + get_devices.metadata = {'url': '/devices'} - def replace_device_twin( - self, id, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): - """Replaces tags and desired properties of a device twin. - - Replaces a device twin. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins - for more information. + def get_identity( + self, id, custom_headers=None, raw=False, **operation_config): + """Gets a device from the identity registry of the IoT Hub. - :param id: Device ID. + :param id: The unique identifier of the device. :type id: str - :param device_twin_info: Device twin info - :type device_twin_info: ~service.models.Twin - :param if_match: - :type if_match: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: Twin or ClientRawResponse if raw=true - :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse + :return: Device or ClientRawResponse if raw=true + :rtype: ~service.models.Device or ~msrest.pipeline.ClientRawResponse :raises: :class:`CloudError` """ # Construct URL - url = self.replace_device_twin.metadata['url'] + url = self.get_identity.metadata['url'] path_format_arguments = { 'id': self._serialize.url("id", id, 'str') } @@ -135,22 +129,15 @@ def replace_device_twin( # Construct headers header_parameters = {} header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' if self.config.generate_client_request_id: header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - # Construct body - # @digimaun - Change deserialize type to {object} from Twin - body_content = self._serialize.body(device_twin_info, '{object}') - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) + request = self._client.get(url, query_parameters, header_parameters) response = self._client.send(request, stream=False, **operation_config) if response.status_code not in [200]: @@ -161,40 +148,39 @@ def replace_device_twin( deserialized = None if response.status_code == 200: - deserialized = self._deserialize('Twin', response) + deserialized = self._deserialize('Device', response) if raw: client_raw_response = ClientRawResponse(deserialized, response) return client_raw_response return deserialized - replace_device_twin.metadata = {'url': '/twins/{id}'} - - def update_device_twin( - self, id, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): - """Updates tags and desired properties of a device twin. + get_identity.metadata = {'url': '/devices/{id}'} - Updates a device twin. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins - for more information. + def create_or_update_identity( + self, id, device, if_match=None, custom_headers=None, raw=False, **operation_config): + """Creates or updates the identity of a device in the identity registry of + the IoT Hub. - :param id: Device ID. + :param id: The unique identifier of the device. :type id: str - :param device_twin_info: Device twin info - :type device_twin_info: ~service.models.Twin - :param if_match: + :param device: The contents of the device identity. + :type device: ~service.models.Device + :param if_match: The string representing a weak ETag for the device + identity, as per RFC7232. This should not be set when creating a + device, but may be set when updating a device. :type if_match: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: Twin or ClientRawResponse if raw=true - :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse + :return: Device or ClientRawResponse if raw=true + :rtype: ~service.models.Device or ~msrest.pipeline.ClientRawResponse :raises: :class:`CloudError` """ # Construct URL - url = self.update_device_twin.metadata['url'] + url = self.create_or_update_identity.metadata['url'] path_format_arguments = { 'id': self._serialize.url("id", id, 'str') } @@ -218,10 +204,10 @@ def update_device_twin( header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') # Construct body - body_content = self._serialize.body(device_twin_info, 'Twin') + body_content = self._serialize.body(device, 'Device') # Construct and send request - request = self._client.patch(url, query_parameters, header_parameters, body_content) + request = self._client.put(url, query_parameters, header_parameters, body_content) response = self._client.send(request, stream=False, **operation_config) if response.status_code not in [200]: @@ -232,27 +218,82 @@ def update_device_twin( deserialized = None if response.status_code == 200: - deserialized = self._deserialize('Twin', response) + deserialized = self._deserialize('Device', response) if raw: client_raw_response = ClientRawResponse(deserialized, response) return client_raw_response return deserialized - update_device_twin.metadata = {'url': '/twins/{id}'} + create_or_update_identity.metadata = {'url': '/devices/{id}'} + + def delete_identity( + self, id, if_match=None, custom_headers=None, raw=False, **operation_config): + """Deletes the identity of a device from the identity registry of the IoT + Hub. + + :param id: The unique identifier of the device. + :type id: str + :param if_match: The string representing a weak ETag for the device + identity, as per RFC7232. The delete operation is performed only if + this ETag matches the value maintained by the server, indicating that + the device identity has not been modified since it was last retrieved. + To force an unconditional delete, set If-Match to the wildcard + character (*). + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.delete_identity.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp - def get_module_twin( - self, id, mid, custom_headers=None, raw=False, **operation_config): - """Gets a module twin. + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete_identity.metadata = {'url': '/devices/{id}'} - Gets a module twin. See + def get_twin( + self, id, custom_headers=None, raw=False, **operation_config): + """Gets the device twin. See https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins for more information. - :param id: Device ID. + :param id: The unique identifier of the device. :type id: str - :param mid: Module ID. - :type mid: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -263,10 +304,9 @@ def get_module_twin( :raises: :class:`CloudError` """ # Construct URL - url = self.get_module_twin.metadata['url'] + url = self.get_twin.metadata['url'] path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') + 'id': self._serialize.url("id", id, 'str') } url = self._client.format_url(url, **path_format_arguments) @@ -303,23 +343,22 @@ def get_module_twin( return client_raw_response return deserialized - get_module_twin.metadata = {'url': '/twins/{id}/modules/{mid}'} + get_twin.metadata = {'url': '/twins/{id}'} - def replace_module_twin( - self, id, mid, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): - """Replaces tags and desired properties of a module twin. - - Replaces a module twin. See + def replace_twin( + self, id, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): + """Replaces the tags and desired properties of a device twin. See https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins for more information. - :param id: Device ID. + :param id: The unique identifier of the device. :type id: str - :param mid: Module ID. - :type mid: str - :param device_twin_info: Device twin info + :param device_twin_info: The device twin info that will replace the + existing info. :type device_twin_info: ~service.models.Twin - :param if_match: + :param if_match: The string representing a weak ETag for the device + twin, as per RFC7232. It determines if the replace operation should be + carried out. :type if_match: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -331,10 +370,9 @@ def replace_module_twin( :raises: :class:`CloudError` """ # Construct URL - url = self.replace_module_twin.metadata['url'] + url = self.replace_twin.metadata['url'] path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') + 'id': self._serialize.url("id", id, 'str') } url = self._client.format_url(url, **path_format_arguments) @@ -378,23 +416,22 @@ def replace_module_twin( return client_raw_response return deserialized - replace_module_twin.metadata = {'url': '/twins/{id}/modules/{mid}'} - - def update_module_twin( - self, id, mid, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): - """Updates tags and desired properties of a module twin. + replace_twin.metadata = {'url': '/twins/{id}'} - Updates a module twin. See + def update_twin( + self, id, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): + """Updates the tags and desired properties of a device twin. See https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins for more information. - :param id: Device ID. + :param id: The unique identifier of the device. :type id: str - :param mid: Module ID. - :type mid: str - :param device_twin_info: Device twin information + :param device_twin_info: The device twin info containing the tags and + desired properties to be updated. :type device_twin_info: ~service.models.Twin - :param if_match: + :param if_match: The string representing a weak ETag for the device + twin, as per RFC7232. It determines if the update operation should be + carried out. :type if_match: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -406,10 +443,9 @@ def update_module_twin( :raises: :class:`CloudError` """ # Construct URL - url = self.update_module_twin.metadata['url'] + url = self.update_twin.metadata['url'] path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') + 'id': self._serialize.url("id", id, 'str') } url = self._client.format_url(url, **path_format_arguments) @@ -452,4 +488,71 @@ def update_module_twin( return client_raw_response return deserialized - update_module_twin.metadata = {'url': '/twins/{id}/modules/{mid}'} + update_twin.metadata = {'url': '/twins/{id}'} + + def invoke_method( + self, device_id, direct_method_request, custom_headers=None, raw=False, **operation_config): + """Invokes a direct method on a device. See + https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-direct-methods + for more information. + + :param device_id: The unique identifier of the device. + :type device_id: str + :param direct_method_request: The parameters to execute a direct + method on the device. + :type direct_method_request: ~service.models.CloudToDeviceMethod + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: CloudToDeviceMethodResult or ClientRawResponse if raw=true + :rtype: ~service.models.CloudToDeviceMethodResult or + ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.invoke_method.metadata['url'] + path_format_arguments = { + 'deviceId': self._serialize.url("device_id", device_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(direct_method_request, 'CloudToDeviceMethod') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('CloudToDeviceMethodResult', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + invoke_method.metadata = {'url': '/twins/{deviceId}/methods'} diff --git a/azext_iot/sdk/iothub/service/operations/digital_twin_operations.py b/azext_iot/sdk/iothub/service/operations/digital_twin_operations.py index ab7279092..9f08e9956 100644 --- a/azext_iot/sdk/iothub/service/operations/digital_twin_operations.py +++ b/azext_iot/sdk/iothub/service/operations/digital_twin_operations.py @@ -23,7 +23,7 @@ class DigitalTwinOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the Api. Constant value: "2019-10-01". + :ivar api_version: Version of the Api. Constant value: "2020-09-30". """ models = models @@ -33,31 +33,29 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2019-10-01" + self.api_version = "2020-09-30" self.config = config - def get_components( - self, digital_twin_id, custom_headers=None, raw=False, **operation_config): - """Gets the list of interfaces. + def get_digital_twin( + self, id, custom_headers=None, raw=False, **operation_config): + """Gets a digital twin. - :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is - DeviceId[~ModuleId]. ModuleId is optional. - :type digital_twin_id: str + :param id: Digital Twin ID. + :type id: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: DigitalTwinInterfaces or ClientRawResponse if raw=true - :rtype: ~service.models.DigitalTwinInterfaces or - ~msrest.pipeline.ClientRawResponse + :return: object or ClientRawResponse if raw=true + :rtype: object or ~msrest.pipeline.ClientRawResponse :raises: :class:`CloudError` """ # Construct URL - url = self.get_components.metadata['url'] + url = self.get_digital_twin.metadata['url'] path_format_arguments = { - 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str') + 'id': self._serialize.url("id", id, 'str') } url = self._client.format_url(url, **path_format_arguments) @@ -88,7 +86,7 @@ def get_components( header_dict = {} if response.status_code == 200: - deserialized = self._deserialize('DigitalTwinInterfaces', response) + deserialized = self._deserialize('object', response) header_dict = { 'ETag': 'str', } @@ -99,37 +97,31 @@ def get_components( return client_raw_response return deserialized - get_components.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces'} + get_digital_twin.metadata = {'url': '/digitaltwins/{id}'} - def update_component( - self, digital_twin_id, if_match=None, interfaces=None, custom_headers=None, raw=False, **operation_config): - """Updates desired properties of multiple interfaces. - Example URI: "digitalTwins/{digitalTwinId}/interfaces". + def update_digital_twin( + self, id, digital_twin_patch, if_match=None, custom_headers=None, raw=False, **operation_config): + """Updates a digital twin. - :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is - DeviceId[~ModuleId]. ModuleId is optional. - :type digital_twin_id: str + :param id: Digital Twin ID. + :type id: str + :param digital_twin_patch: json-patch contents to update. + :type digital_twin_patch: list[object] :param if_match: :type if_match: str - :param interfaces: Interface(s) data to patch in the digital twin. - :type interfaces: dict[str, - ~service.models.DigitalTwinInterfacesPatchInterfacesValue] :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: DigitalTwinInterfaces or ClientRawResponse if raw=true - :rtype: ~service.models.DigitalTwinInterfaces or - ~msrest.pipeline.ClientRawResponse + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse :raises: :class:`CloudError` """ - interfaces_patch_info = models.DigitalTwinInterfacesPatch(interfaces=interfaces) - # Construct URL - url = self.update_component.metadata['url'] + url = self.update_digital_twin.metadata['url'] path_format_arguments = { - 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str') + 'id': self._serialize.url("id", id, 'str') } url = self._client.format_url(url, **path_format_arguments) @@ -139,7 +131,6 @@ def update_component( # Construct headers header_parameters = {} - header_parameters['Accept'] = 'application/json' header_parameters['Content-Type'] = 'application/json; charset=utf-8' if self.config.generate_client_request_id: header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) @@ -151,116 +142,44 @@ def update_component( header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') # Construct body - body_content = self._serialize.body(interfaces_patch_info, 'DigitalTwinInterfacesPatch') + body_content = self._serialize.body(digital_twin_patch, '[object]') # Construct and send request request = self._client.patch(url, query_parameters, header_parameters, body_content) response = self._client.send(request, stream=False, **operation_config) - if response.status_code not in [200]: + if response.status_code not in [202]: exp = CloudError(response) exp.request_id = response.headers.get('x-ms-request-id') raise exp - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('DigitalTwinInterfaces', response) - header_dict = { - 'ETag': 'str', - } - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) + client_raw_response = ClientRawResponse(None, response) + client_raw_response.add_headers({ + 'ETag': 'str', + 'Location': 'str', + }) return client_raw_response + update_digital_twin.metadata = {'url': '/digitaltwins/{id}'} - return deserialized - update_component.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces'} - - def get_component( - self, digital_twin_id, interface_name, custom_headers=None, raw=False, **operation_config): - """Gets the interface of given interfaceId. - Example URI: "digitalTwins/{digitalTwinId}/interfaces/{interfaceName}". - - :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is - DeviceId[~ModuleId]. ModuleId is optional. - :type digital_twin_id: str - :param interface_name: The interface name. - :type interface_name: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: DigitalTwinInterfaces or ClientRawResponse if raw=true - :rtype: ~service.models.DigitalTwinInterfaces or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_component.metadata['url'] - path_format_arguments = { - 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str'), - 'interfaceName': self._serialize.url("interface_name", interface_name, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('DigitalTwinInterfaces', response) - header_dict = { - 'ETag': 'str', - } + def invoke_root_level_command( + self, id, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config): + """Invoke a digital twin root level command. - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response + Invoke a digital twin root level command. - return deserialized - get_component.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces/{interfaceName}'} - - def get_digital_twin_model( - self, model_id, expand=None, custom_headers=None, raw=False, **operation_config): - """Returns a DigitalTwin model definition for the given id. - If "expand" is present in the query parameters and id is for a device - capability model then it returns - the capability metamodel with expanded interface definitions. - - :param model_id: Model id Ex: - urn:contoso:TemperatureSensor:1 - :type model_id: str - :param expand: Indicates whether to expand the device capability - model's interface definitions inline or not. - This query parameter ONLY applies to Capability model. - :type expand: bool + :param id: + :type id: str + :param command_name: + :type command_name: str + :param payload: + :type payload: object + :param connect_timeout_in_seconds: Maximum interval of time, in + seconds, that the digital twin command will wait for the answer. + :type connect_timeout_in_seconds: int + :param response_timeout_in_seconds: Maximum interval of time, in + seconds, that the digital twin command will wait for the answer. + :type response_timeout_in_seconds: int :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -271,21 +190,25 @@ def get_digital_twin_model( :raises: :class:`CloudError` """ # Construct URL - url = self.get_digital_twin_model.metadata['url'] + url = self.invoke_root_level_command.metadata['url'] path_format_arguments = { - 'modelId': self._serialize.url("model_id", model_id, 'str') + 'id': self._serialize.url("id", id, 'str'), + 'commandName': self._serialize.url("command_name", command_name, 'str') } url = self._client.format_url(url, **path_format_arguments) # Construct parameters query_parameters = {} - if expand is not None: - query_parameters['expand'] = self._serialize.query("expand", expand, 'bool') query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + if connect_timeout_in_seconds is not None: + query_parameters['connectTimeoutInSeconds'] = self._serialize.query("connect_timeout_in_seconds", connect_timeout_in_seconds, 'int') + if response_timeout_in_seconds is not None: + query_parameters['responseTimeoutInSeconds'] = self._serialize.query("response_timeout_in_seconds", response_timeout_in_seconds, 'int') # Construct headers header_parameters = {} header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' if self.config.generate_client_request_id: header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: @@ -293,11 +216,14 @@ def get_digital_twin_model( if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + # Construct body + body_content = self._serialize.body(payload, 'object') + # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) + request = self._client.post(url, query_parameters, header_parameters, body_content) response = self._client.send(request, stream=False, **operation_config) - if response.status_code not in [200, 204]: + if response.status_code not in [200]: exp = CloudError(response) exp.request_id = response.headers.get('x-ms-request-id') raise exp @@ -308,10 +234,8 @@ def get_digital_twin_model( if response.status_code == 200: deserialized = self._deserialize('object', response) header_dict = { - 'ETag': 'str', - 'x-ms-model-id': 'str', - 'x-ms-model-resolution-status': 'str', - 'x-ms-model-resolution-description': 'str', + 'x-ms-command-statuscode': 'int', + 'x-ms-request-id': 'str', } if raw: @@ -320,25 +244,27 @@ def get_digital_twin_model( return client_raw_response return deserialized - get_digital_twin_model.metadata = {'url': '/digitalTwins/models/{modelId}'} + invoke_root_level_command.metadata = {'url': '/digitaltwins/{id}/commands/{commandName}'} def invoke_component_command( - self, digital_twin_id, interface_name, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config): - """Invoke a digital twin interface command. + self, id, component_path, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config): + """Invoke a digital twin command. - Invoke a digital twin interface command. + Invoke a digital twin command. - :param digital_twin_id: - :type digital_twin_id: str - :param interface_name: - :type interface_name: str + :param id: + :type id: str + :param component_path: + :type component_path: str :param command_name: :type command_name: str :param payload: :type payload: object - :param connect_timeout_in_seconds: Connect timeout in seconds. + :param connect_timeout_in_seconds: Maximum interval of time, in + seconds, that the digital twin command will wait for the answer. :type connect_timeout_in_seconds: int - :param response_timeout_in_seconds: Response timeout in seconds. + :param response_timeout_in_seconds: Maximum interval of time, in + seconds, that the digital twin command will wait for the answer. :type response_timeout_in_seconds: int :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -352,8 +278,8 @@ def invoke_component_command( # Construct URL url = self.invoke_component_command.metadata['url'] path_format_arguments = { - 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str'), - 'interfaceName': self._serialize.url("interface_name", interface_name, 'str'), + 'id': self._serialize.url("id", id, 'str'), + 'componentPath': self._serialize.url("component_path", component_path, 'str'), 'commandName': self._serialize.url("command_name", command_name, 'str') } url = self._client.format_url(url, **path_format_arguments) @@ -405,4 +331,4 @@ def invoke_component_command( return client_raw_response return deserialized - invoke_component_command.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces/{interfaceName}/commands/{commandName}'} + invoke_component_command.metadata = {'url': '/digitaltwins/{id}/components/{componentPath}/commands/{commandName}'} diff --git a/azext_iot/sdk/iothub/service/operations/job_client_operations.py b/azext_iot/sdk/iothub/service/operations/jobs_operations.py similarity index 89% rename from azext_iot/sdk/iothub/service/operations/job_client_operations.py rename to azext_iot/sdk/iothub/service/operations/jobs_operations.py index 215dd9d57..b6ddc14de 100644 --- a/azext_iot/sdk/iothub/service/operations/job_client_operations.py +++ b/azext_iot/sdk/iothub/service/operations/jobs_operations.py @@ -16,14 +16,14 @@ from .. import models -class JobClientOperations(object): - """JobClientOperations operations. +class JobsOperations(object): + """JobsOperations operations. :param client: Client for service requests. :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the Api. Constant value: "2019-10-01". + :ivar api_version: Version of the Api. Constant value: "2020-09-30". """ models = models @@ -33,19 +33,17 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2019-10-01" + self.api_version = "2020-09-30" self.config = config def create_import_export_job( self, job_properties, custom_headers=None, raw=False, **operation_config): - """Create a new import/export job on an IoT hub. - - Create a new import/export job on an IoT hub. See + """Creates a new import or export job on the IoT Hub. See https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities for more information. - :param job_properties: + :param job_properties: The job specifications. :type job_properties: ~service.models.JobProperties :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -101,9 +99,7 @@ def create_import_export_job( def get_import_export_jobs( self, custom_headers=None, raw=False, **operation_config): - """Gets the status of all import/export jobs in an iot hub. - - Gets the status of all import/export jobs in an iot hub. See + """Gets the status of all import and export jobs in the IoT Hub. See https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities for more information. @@ -157,13 +153,11 @@ def get_import_export_jobs( def get_import_export_job( self, id, custom_headers=None, raw=False, **operation_config): - """Gets the status of an import or export job in an iot hub. - - Gets the status of an import or export job in an iot hub. See + """Gets the status of an import or export job in the IoT Hub. See https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities for more information. - :param id: Job ID. + :param id: The unique identifier of the job. :type id: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -219,13 +213,9 @@ def get_import_export_job( def cancel_import_export_job( self, id, custom_headers=None, raw=False, **operation_config): - """Cancels an import or export job in an IoT hub. - - Cancels an import or export job in an IoT hub. See - https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities - for more information. + """Cancels an import or export job in the IoT Hub. - :param id: Job ID. + :param id: The unique identifier of the job. :type id: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -278,15 +268,13 @@ def cancel_import_export_job( return deserialized cancel_import_export_job.metadata = {'url': '/jobs/{id}'} - def get_job( + def get_scheduled_job( self, id, custom_headers=None, raw=False, **operation_config): - """Retrieves details of a scheduled job from an IoT hub. - - Retrieves details of a scheduled job from an IoT hub. See + """Gets details of a scheduled job from the IoT Hub. See https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs for more information. - :param id: Job ID. + :param id: The unique identifier of the job. :type id: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -299,7 +287,7 @@ def get_job( :raises: :class:`CloudError` """ # Construct URL - url = self.get_job.metadata['url'] + url = self.get_scheduled_job.metadata['url'] path_format_arguments = { 'id': self._serialize.url("id", id, 'str') } @@ -338,21 +326,18 @@ def get_job( return client_raw_response return deserialized - get_job.metadata = {'url': '/jobs/v2/{id}'} + get_scheduled_job.metadata = {'url': '/jobs/v2/{id}'} - def create_job( + def create_scheduled_job( self, id, job_request, custom_headers=None, raw=False, **operation_config): - """Creates a new job to schedule update twins or device direct methods on - an IoT hub at a scheduled time. - - Creates a new job to schedule update twins or device direct methods on - an IoT hub at a scheduled time. See + """Creates a new job to schedule twin updates or direct methods on the IoT + Hub at a scheduled time. See https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs for more information. - :param id: Job ID. + :param id: The unique identifier of the job. :type id: str - :param job_request: + :param job_request: The job request info. :type job_request: ~service.models.JobRequest :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -365,7 +350,7 @@ def create_job( :raises: :class:`CloudError` """ # Construct URL - url = self.create_job.metadata['url'] + url = self.create_scheduled_job.metadata['url'] path_format_arguments = { 'id': self._serialize.url("id", id, 'str') } @@ -408,17 +393,15 @@ def create_job( return client_raw_response return deserialized - create_job.metadata = {'url': '/jobs/v2/{id}'} + create_scheduled_job.metadata = {'url': '/jobs/v2/{id}'} - def cancel_job( + def cancel_scheduled_job( self, id, custom_headers=None, raw=False, **operation_config): - """Cancels a scheduled job on an IoT hub. - - Cancels a scheduled job on an IoT hub. See + """Cancels a scheduled job on the IoT Hub. See https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs for more information. - :param id: Job ID. + :param id: The unique identifier of the job. :type id: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -431,7 +414,7 @@ def cancel_job( :raises: :class:`CloudError` """ # Construct URL - url = self.cancel_job.metadata['url'] + url = self.cancel_scheduled_job.metadata['url'] path_format_arguments = { 'id': self._serialize.url("id", id, 'str') } @@ -470,22 +453,21 @@ def cancel_job( return client_raw_response return deserialized - cancel_job.metadata = {'url': '/jobs/v2/{id}/cancel'} + cancel_scheduled_job.metadata = {'url': '/jobs/v2/{id}/cancel'} - def query_jobs( + def query_scheduled_jobs( self, job_type=None, job_status=None, custom_headers=None, raw=False, **operation_config): - """Query an IoT hub to retrieve information regarding jobs using the IoT - Hub query language. - - Query an IoT hub to retrieve information regarding jobs using the IoT - Hub query language. See + """Gets the information about jobs using an IoT Hub query. See https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-query-language - for more information. Pagination of results is supported. This returns - information about jobs only. + for more information. - :param job_type: Job Type. + :param job_type: The job type. See + https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs#querying-for-progress-on-jobs + for a list of possible job types. :type job_type: str - :param job_status: Job Status. + :param job_status: The job status. See + https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs#querying-for-progress-on-jobs + for a list of possible statuses. :type job_status: str :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the @@ -498,7 +480,7 @@ def query_jobs( :raises: :class:`CloudError` """ # Construct URL - url = self.query_jobs.metadata['url'] + url = self.query_scheduled_jobs.metadata['url'] # Construct parameters query_parameters = {} @@ -548,4 +530,4 @@ def query_jobs( return client_raw_response return deserialized - query_jobs.metadata = {'url': '/jobs/v2/query'} + query_scheduled_jobs.metadata = {'url': '/jobs/v2/query'} diff --git a/azext_iot/sdk/iothub/service/operations/modules_operations.py b/azext_iot/sdk/iothub/service/operations/modules_operations.py new file mode 100644 index 000000000..338c512b5 --- /dev/null +++ b/azext_iot/sdk/iothub/service/operations/modules_operations.py @@ -0,0 +1,574 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrestazure.azure_exceptions import CloudError + +from .. import models + + +class ModulesOperations(object): + """ModulesOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the Api. Constant value: "2020-09-30". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-09-30" + + self.config = config + + def get_twin( + self, id, mid, custom_headers=None, raw=False, **operation_config): + """Gets the module twin. See + https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins + for more information. + + :param id: The unique identifier of the device. + :type id: str + :param mid: The unique identifier of the module. + :type mid: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: Twin or ClientRawResponse if raw=true + :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.get_twin.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'mid': self._serialize.url("mid", mid, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('Twin', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_twin.metadata = {'url': '/twins/{id}/modules/{mid}'} + + def replace_twin( + self, id, mid, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): + """Replaces the tags and desired properties of a module twin. See + https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins + for more information. + + :param id: The unique identifier of the device. + :type id: str + :param mid: The unique identifier of the module. + :type mid: str + :param device_twin_info: The module twin info that will replace the + existing info. + :type device_twin_info: ~service.models.Twin + :param if_match: The string representing a weak ETag for the device + twin, as per RFC7232. It determines if the replace operation should be + carried out. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: Twin or ClientRawResponse if raw=true + :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.replace_twin.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'mid': self._serialize.url("mid", mid, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + # @digimaun - Change deserialize type to {object} from Twin + body_content = self._serialize.body(device_twin_info, '{object}') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('Twin', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + replace_twin.metadata = {'url': '/twins/{id}/modules/{mid}'} + + def update_twin( + self, id, mid, device_twin_info, if_match=None, custom_headers=None, raw=False, **operation_config): + """Updates the tags and desired properties of a module twin. See + https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-device-twins + for more information. + + :param id: The unique identifier of the device. + :type id: str + :param mid: The unique identifier of the module. + :type mid: str + :param device_twin_info: The module twin info containing the tags and + desired properties to be updated. + :type device_twin_info: ~service.models.Twin + :param if_match: The string representing a weak ETag for the device + twin, as per RFC7232. It determines if the update operation should be + carried out. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: Twin or ClientRawResponse if raw=true + :rtype: ~service.models.Twin or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.update_twin.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'mid': self._serialize.url("mid", mid, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(device_twin_info, 'Twin') + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('Twin', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + update_twin.metadata = {'url': '/twins/{id}/modules/{mid}'} + + def get_modules_on_device( + self, id, custom_headers=None, raw=False, **operation_config): + """Gets all the module identities on the device. + + :param id: The unique identifier of the device. + :type id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: list or ClientRawResponse if raw=true + :rtype: list[~service.models.Module] or + ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.get_modules_on_device.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('[Module]', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_modules_on_device.metadata = {'url': '/devices/{id}/modules'} + + def get_identity( + self, id, mid, custom_headers=None, raw=False, **operation_config): + """Gets a module identity on the device. + + :param id: The unique identifier of the device. + :type id: str + :param mid: The unique identifier of the module. + :type mid: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: Module or ClientRawResponse if raw=true + :rtype: ~service.models.Module or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.get_identity.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'mid': self._serialize.url("mid", mid, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('Module', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get_identity.metadata = {'url': '/devices/{id}/modules/{mid}'} + + def create_or_update_identity( + self, id, mid, module, if_match=None, custom_headers=None, raw=False, **operation_config): + """Creates or updates the module identity for a device in the IoT Hub. The + moduleId and generationId cannot be updated by the user. + + :param id: The unique identifier of the device. + :type id: str + :param mid: The unique identifier of the module. + :type mid: str + :param module: The module identity. + :type module: ~service.models.Module + :param if_match: The string representing a weak ETag for the module, + as per RFC7232. This should not be set when creating a module, but may + be set when updating a module. + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: Module or ClientRawResponse if raw=true + :rtype: ~service.models.Module or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.create_or_update_identity.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'mid': self._serialize.url("mid", mid, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(module, 'Module') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 201]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('Module', response) + if response.status_code == 201: + deserialized = self._deserialize('Module', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create_or_update_identity.metadata = {'url': '/devices/{id}/modules/{mid}'} + + def delete_identity( + self, id, mid, if_match=None, custom_headers=None, raw=False, **operation_config): + """Deletes the module identity for a device in the IoT Hub. + + :param id: The unique identifier of the deivce. + :type id: str + :param mid: The unique identifier of the module. + :type mid: str + :param if_match: The string representing a weak ETag for the module, + as per RFC7232. The delete operation is performed only if this ETag + matches the value maintained by the server, indicating that the module + has not been modified since it was last retrieved. To force an + unconditional delete, set If-Match to the wildcard character (*). + :type if_match: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.delete_identity.metadata['url'] + path_format_arguments = { + 'id': self._serialize.url("id", id, 'str'), + 'mid': self._serialize.url("mid", mid, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if if_match is not None: + header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [204]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete_identity.metadata = {'url': '/devices/{id}/modules/{mid}'} + + def invoke_method( + self, device_id, module_id, direct_method_request, custom_headers=None, raw=False, **operation_config): + """Invokes a direct method on a module of a device. See + https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-direct-methods + for more information. + + :param device_id: The unique identifier of the device. + :type device_id: str + :param module_id: The unique identifier of the module. + :type module_id: str + :param direct_method_request: The parameters to execute a direct + method on the module. + :type direct_method_request: ~service.models.CloudToDeviceMethod + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: CloudToDeviceMethodResult or ClientRawResponse if raw=true + :rtype: ~service.models.CloudToDeviceMethodResult or + ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + # Construct URL + url = self.invoke_method.metadata['url'] + path_format_arguments = { + 'deviceId': self._serialize.url("device_id", device_id, 'str'), + 'moduleId': self._serialize.url("module_id", module_id, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(direct_method_request, 'CloudToDeviceMethod') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('CloudToDeviceMethodResult', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + invoke_method.metadata = {'url': '/twins/{deviceId}/modules/{moduleId}/methods'} diff --git a/azext_iot/sdk/iothub/service/operations/query_operations.py b/azext_iot/sdk/iothub/service/operations/query_operations.py new file mode 100644 index 000000000..33a9f4543 --- /dev/null +++ b/azext_iot/sdk/iothub/service/operations/query_operations.py @@ -0,0 +1,106 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrestazure.azure_exceptions import CloudError + +from .. import models + + +class QueryOperations(object): + """QueryOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the Api. Constant value: "2020-09-30". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-09-30" + + self.config = config + + def get_twins( + self, query=None, x_ms_continuation=None, x_ms_max_item_count=None, custom_headers=None, raw=False, **operation_config): + """Query an IoT Hub to retrieve information regarding device twins using a + SQL-like language. See + https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-query-language + for more information. Pagination is supported. This returns information + about device twins only. + + :param x_ms_continuation: The continuation token used to get the next + page of results. + :type x_ms_continuation: str + :param x_ms_max_item_count: The maximum number of items returned per + page. The service may use a different value if the value specified is + not acceptable. + :type x_ms_max_item_count: str + :param query: The query string. + :type query: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: list or ClientRawResponse if raw=true + :rtype: list[~service.models.Twin] or + ~msrest.pipeline.ClientRawResponse + :raises: :class:`CloudError` + """ + query_specification = models.QuerySpecification(query=query) + + # Construct URL + url = self.get_twins.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if x_ms_continuation is not None: + header_parameters['x-ms-continuation'] = self._serialize.header("x_ms_continuation", x_ms_continuation, 'str') + if x_ms_max_item_count is not None: + header_parameters['x-ms-max-item-count'] = self._serialize.header("x_ms_max_item_count", x_ms_max_item_count, 'str') + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(query_specification, 'QuerySpecification') + + # Construct and send request + request = self._client.post(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + exp = CloudError(response) + exp.request_id = response.headers.get('x-ms-request-id') + raise exp + + # @digimaun - custom work, cut the fluff + return ClientRawResponse(None, response) + + get_twins.metadata = {'url': '/devices/query'} diff --git a/azext_iot/sdk/iothub/service/operations/registry_manager_operations.py b/azext_iot/sdk/iothub/service/operations/registry_manager_operations.py deleted file mode 100644 index 4ddea8358..000000000 --- a/azext_iot/sdk/iothub/service/operations/registry_manager_operations.py +++ /dev/null @@ -1,857 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -import uuid -from msrest.pipeline import ClientRawResponse -from msrestazure.azure_exceptions import CloudError - -from .. import models - - -class RegistryManagerOperations(object): - """RegistryManagerOperations operations. - - :param client: Client for service requests. - :param config: Configuration of service client. - :param serializer: An object model serializer. - :param deserializer: An object model deserializer. - :ivar api_version: Version of the Api. Constant value: "2019-10-01". - """ - - models = models - - def __init__(self, client, config, serializer, deserializer): - - self._client = client - self._serialize = serializer - self._deserialize = deserializer - self.api_version = "2019-10-01" - - self.config = config - - def get_device_statistics( - self, custom_headers=None, raw=False, **operation_config): - """Retrieves statistics about device identities in the IoT hub’s identity - registry. - - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: RegistryStatistics or ClientRawResponse if raw=true - :rtype: ~service.models.RegistryStatistics or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_device_statistics.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('RegistryStatistics', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_device_statistics.metadata = {'url': '/statistics/devices'} - - def get_service_statistics( - self, custom_headers=None, raw=False, **operation_config): - """Retrieves service statistics for this IoT hub’s identity registry. - - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: ServiceStatistics or ClientRawResponse if raw=true - :rtype: ~service.models.ServiceStatistics or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_service_statistics.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('ServiceStatistics', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_service_statistics.metadata = {'url': '/statistics/service'} - - def get_devices( - self, top=None, custom_headers=None, raw=False, **operation_config): - """Get the identities of multiple devices from the IoT hub identity - registry. Not recommended. Use the IoT Hub query language to retrieve - device twin and device identity information. See - https://docs.microsoft.com/en-us/rest/api/iothub/service/queryiothub - and - https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-query-language - for more information. - - :param top: This parameter when specified, defines the maximum number - of device identities that are returned. Any value outside the range of - 1-1000 is considered to be 1000. - :type top: int - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~service.models.Device] or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_devices.metadata['url'] - - # Construct parameters - query_parameters = {} - if top is not None: - query_parameters['top'] = self._serialize.query("top", top, 'int') - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[Device]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_devices.metadata = {'url': '/devices'} - - def bulk_device_crud( - self, devices, custom_headers=None, raw=False, **operation_config): - """Create, update, or delete the identities of multiple devices from the - IoT hub identity registry. - - Create, update, or delete the identiies of multiple devices from the - IoT hub identity registry. A device identity can be specified only once - in the list. Different operations (create, update, delete) on different - devices are allowed. A maximum of 100 devices can be specified per - invocation. For large scale operations, consider using the import - feature using blob - storage(https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-identity-registry#import-and-export-device-identities). - - :param devices: - :type devices: list[~service.models.ExportImportDevice] - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: BulkRegistryOperationResult or ClientRawResponse if raw=true - :rtype: ~service.models.BulkRegistryOperationResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.bulk_device_crud.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(devices, '[ExportImportDevice]') - - # Construct and send request - request = self._client.post(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200, 400]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('BulkRegistryOperationResult', response) - if response.status_code == 400: - deserialized = self._deserialize('BulkRegistryOperationResult', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - bulk_device_crud.metadata = {'url': '/devices'} - - def query_iot_hub( - self, query=None, x_ms_continuation=None, x_ms_max_item_count=None, custom_headers=None, raw=False, **operation_config): - """Query an IoT hub to retrieve information regarding device twins using a - SQL-like language. - - Query an IoT hub to retrieve information regarding device twins using a - SQL-like language. See - https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-query-language - for more information. Pagination of results is supported. This returns - information about device twins only. - - :param x_ms_continuation: - :type x_ms_continuation: str - :param x_ms_max_item_count: - :type x_ms_max_item_count: str - :param query: The query. - :type query: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~service.models.Twin] or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - query_specification = models.QuerySpecification(query=query) - - # Construct URL - url = self.query_iot_hub.metadata['url'] - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if x_ms_continuation is not None: - header_parameters['x-ms-continuation'] = self._serialize.header("x_ms_continuation", x_ms_continuation, 'str') - if x_ms_max_item_count is not None: - header_parameters['x-ms-max-item-count'] = self._serialize.header("x_ms_max_item_count", x_ms_max_item_count, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(query_specification, 'QuerySpecification') - - # Construct and send request - request = self._client.post(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - # @digimaun - custom work, cut the fluff - return ClientRawResponse(None, response) - - query_iot_hub.metadata = {'url': '/devices/query'} - - def get_device( - self, id, custom_headers=None, raw=False, **operation_config): - """Retrieve a device from the identity registry of an IoT hub. - - Retrieve a device from the identity registry of an IoT hub. - - :param id: Device ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Device or ClientRawResponse if raw=true - :rtype: ~service.models.Device or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_device.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Device', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_device.metadata = {'url': '/devices/{id}'} - - def create_or_update_device( - self, id, device, if_match=None, custom_headers=None, raw=False, **operation_config): - """Create or update the identity of a device in the identity registry of - an IoT hub. - - Create or update the identity of a device in the identity registry of - an IoT hub. An ETag must not be specified for the create operation. An - ETag must be specified for the update operation. Note that generationId - and deviceId cannot be updated by the user. - - :param id: Device ID. - :type id: str - :param device: - :type device: ~service.models.Device - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Device or ClientRawResponse if raw=true - :rtype: ~service.models.Device or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.create_or_update_device.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(device, 'Device') - - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Device', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_or_update_device.metadata = {'url': '/devices/{id}'} - - def delete_device( - self, id, if_match=None, custom_headers=None, raw=False, **operation_config): - """Delete the identity of a device from the identity registry of an IoT - hub. - - Delete the identity of a device from the identity registry of an IoT - hub. This request requires the If-Match header. The client may specify - the ETag for the device identity on the request in order to compare to - the ETag maintained by the service for the purpose of optimistic - concurrency. The delete operation is performed only if the ETag sent by - the client matches the value maintained by the server, indicating that - the device identity has not been modified since it was retrieved by the - client. To force an unconditional delete, set If-Match to the wildcard - character (*). - - :param id: Device ID. - :type id: str - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.delete_device.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [204]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - delete_device.metadata = {'url': '/devices/{id}'} - - def purge_command_queue( - self, id, custom_headers=None, raw=False, **operation_config): - """Deletes all the pending commands for this device from the IoT hub. - - Deletes all the pending commands for this device from the IoT hub. - - :param id: Device ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: PurgeMessageQueueResult or ClientRawResponse if raw=true - :rtype: ~service.models.PurgeMessageQueueResult or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.purge_command_queue.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('PurgeMessageQueueResult', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - purge_command_queue.metadata = {'url': '/devices/{id}/commands'} - - def get_modules_on_device( - self, id, custom_headers=None, raw=False, **operation_config): - """Retrieve all the module identities on the device. - - :param id: Device ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: list or ClientRawResponse if raw=true - :rtype: list[~service.models.Module] or - ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_modules_on_device.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('[Module]', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_modules_on_device.metadata = {'url': '/devices/{id}/modules'} - - def get_module( - self, id, mid, custom_headers=None, raw=False, **operation_config): - """Retrieve the specified module identity on the device. - - :param id: Device ID. - :type id: str - :param mid: Module ID. - :type mid: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Module or ClientRawResponse if raw=true - :rtype: ~service.models.Module or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_module.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Module', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - get_module.metadata = {'url': '/devices/{id}/modules/{mid}'} - - def create_or_update_module( - self, id, mid, module, if_match=None, custom_headers=None, raw=False, **operation_config): - """Create or update the module identity for device in IoT hub. An ETag - must not be specified for the create operation. An ETag must be - specified for the update operation. Note that moduleId and generation - cannot be updated by the user. - - :param id: Device ID. - :type id: str - :param mid: Module ID. - :type mid: str - :param module: - :type module: ~service.models.Module - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: Module or ClientRawResponse if raw=true - :rtype: ~service.models.Module or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.create_or_update_module.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(module, 'Module') - - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200, 201]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - - if response.status_code == 200: - deserialized = self._deserialize('Module', response) - if response.status_code == 201: - deserialized = self._deserialize('Module', response) - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response - - return deserialized - create_or_update_module.metadata = {'url': '/devices/{id}/modules/{mid}'} - - def delete_module( - self, id, mid, if_match=None, custom_headers=None, raw=False, **operation_config): - """Delete the module identity for device of an IoT hub. This request - requires the If-Match header. The client may specify the ETag for the - device identity on the request in order to compare to the ETag - maintained by the service for the purpose of optimistic concurrency. - The delete operation is performed only if the ETag sent by the client - matches the value maintained by the server, indicating that the device - identity has not been modified since it was retrieved by the client. To - force an unconditional delete, set If-Match to the wildcard character - (*). - - :param id: Device ID. - :type id: str - :param mid: Module ID. - :type mid: str - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.delete_module.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'mid': self._serialize.url("mid", mid, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.delete(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [204]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - delete_module.metadata = {'url': '/devices/{id}/modules/{mid}'} diff --git a/azext_iot/sdk/iothub/service/operations/fault_injection_operations.py b/azext_iot/sdk/iothub/service/operations/statistics_operations.py similarity index 73% rename from azext_iot/sdk/iothub/service/operations/fault_injection_operations.py rename to azext_iot/sdk/iothub/service/operations/statistics_operations.py index 49ddc4d1f..b6060f1ec 100644 --- a/azext_iot/sdk/iothub/service/operations/fault_injection_operations.py +++ b/azext_iot/sdk/iothub/service/operations/statistics_operations.py @@ -16,14 +16,14 @@ from .. import models -class FaultInjectionOperations(object): - """FaultInjectionOperations operations. +class StatisticsOperations(object): + """StatisticsOperations operations. :param client: Client for service requests. :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the Api. Constant value: "2019-10-01". + :ivar api_version: Version of the Api. Constant value: "2020-09-30". """ models = models @@ -33,26 +33,27 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2019-10-01" + self.api_version = "2020-09-30" self.config = config - def get( + def get_device_statistics( self, custom_headers=None, raw=False, **operation_config): - """Get FaultInjection entity. + """Gets device statistics of the IoT Hub identity registry, such as total + device count. :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: FaultInjectionProperties or ClientRawResponse if raw=true - :rtype: ~service.models.FaultInjectionProperties or + :return: RegistryStatistics or ClientRawResponse if raw=true + :rtype: ~service.models.RegistryStatistics or ~msrest.pipeline.ClientRawResponse :raises: :class:`CloudError` """ # Construct URL - url = self.get.metadata['url'] + url = self.get_device_statistics.metadata['url'] # Construct parameters query_parameters = {} @@ -80,32 +81,32 @@ def get( deserialized = None if response.status_code == 200: - deserialized = self._deserialize('FaultInjectionProperties', response) + deserialized = self._deserialize('RegistryStatistics', response) if raw: client_raw_response = ClientRawResponse(deserialized, response) return client_raw_response return deserialized - get.metadata = {'url': '/faultInjection'} + get_device_statistics.metadata = {'url': '/statistics/devices'} - def set( - self, value, custom_headers=None, raw=False, **operation_config): - """Create or update FaultInjection entity. + def get_service_statistics( + self, custom_headers=None, raw=False, **operation_config): + """Gets service statistics of the IoT Hub identity registry, such as + connected device count. - :param value: - :type value: ~service.models.FaultInjectionProperties :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse + :return: ServiceStatistics or ClientRawResponse if raw=true + :rtype: ~service.models.ServiceStatistics or + ~msrest.pipeline.ClientRawResponse :raises: :class:`CloudError` """ # Construct URL - url = self.set.metadata['url'] + url = self.get_service_statistics.metadata['url'] # Construct parameters query_parameters = {} @@ -113,7 +114,7 @@ def set( # Construct headers header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' + header_parameters['Accept'] = 'application/json' if self.config.generate_client_request_id: header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: @@ -121,11 +122,8 @@ def set( if self.config.accept_language is not None: header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - # Construct body - body_content = self._serialize.body(value, 'FaultInjectionProperties') - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) + request = self._client.get(url, query_parameters, header_parameters) response = self._client.send(request, stream=False, **operation_config) if response.status_code not in [200]: @@ -133,7 +131,14 @@ def set( exp.request_id = response.headers.get('x-ms-request-id') raise exp + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize('ServiceStatistics', response) + if raw: - client_raw_response = ClientRawResponse(None, response) + client_raw_response = ClientRawResponse(deserialized, response) return client_raw_response - set.metadata = {'url': '/faultInjection'} + + return deserialized + get_service_statistics.metadata = {'url': '/statistics/service'} diff --git a/azext_iot/sdk/iothub/service/version.py b/azext_iot/sdk/iothub/service/version.py index f0880ef49..f4d73cc97 100644 --- a/azext_iot/sdk/iothub/service/version.py +++ b/azext_iot/sdk/iothub/service/version.py @@ -9,5 +9,5 @@ # regenerated. # -------------------------------------------------------------------------- -VERSION = "2019-10-01" +VERSION = "2020-09-30" diff --git a/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py b/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py index 6334b51a8..2d4aff133 100644 --- a/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py +++ b/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py @@ -264,8 +264,7 @@ def test_job_create( if job_type == JobType.scheduleUpdateTwin.value: assert body["type"] == JobType.scheduleUpdateTwin.value - payload["etag"] = "*" - assert body["updateTwin"] == payload + assert body["updateTwin"] == {'etag': '*'} elif job_type == JobType.scheduleDeviceMethod.value: assert body["type"] == JobType.scheduleDeviceMethod.value assert body["cloudToDeviceMethod"]["methodName"] == method_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 new file mode 100644 index 000000000..dee8cb20e --- /dev/null +++ b/azext_iot/tests/iothub/test_iothub_nested_edge_int.py @@ -0,0 +1,243 @@ +# 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/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 733cb7971..93cfbeb39 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -360,6 +360,7 @@ def test_hub_devices(self): 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), ], ) @@ -1529,7 +1530,7 @@ def test_edge_offline(self): "iot hub device-identity set-parent -d {} --pd {} -n {} -g {}".format( edge_device_ids[0], edge_device_ids[1], LIVE_HUB, LIVE_RG ), - expect_failure=True, + checks=self.is_empty(), ) # add device as a child of non-edge device @@ -1580,7 +1581,7 @@ def test_edge_offline(self): "iot hub device-identity add-children -d {} --child-list {} -n {} -g {}".format( edge_device_ids[0], device_ids[0], LIVE_HUB, LIVE_RG ), - checks=self.is_empty(), + expect_failure=True, ) # add same device as a child of another edge device @@ -1607,7 +1608,6 @@ def test_edge_offline(self): expect_failure=False, ) - # TODO: Result should be JSON expected_output = "{}".format(device_ids[1]) assert output.get_output_in_json() == expected_output diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index fdb740ed7..02555b21d 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -200,12 +200,12 @@ def sc_invalid_args_device_create_setparent( self, mocker, fixture_ghcs, fixture_sas, request ): service_client = mocker.patch(path_service_client) - edge_kvp = {} + parent_kvp = {} if request.param[1] == 0: - edge_kvp.setdefault("capabilities", {"iotEdge": False}) + parent_kvp.setdefault("capabilities", {"iotEdge": False}) test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_parent_device(**edge_kvp) + mocker, request.param[0], generate_parent_device(**parent_kvp) ) ] service_client.side_effect = test_side_effect @@ -231,19 +231,21 @@ def test_device_create_setparent_invalid_args( device_id, ) - @pytest.fixture(params=[(200, 0), (200, 1)]) + @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) - nonedge_kvp = {} + child_kvp = {} + if request.param[1] == 1: + child_kvp.setdefault("parentScopes", ["abcd"]) if request.param[1] == 1: - nonedge_kvp.setdefault("deviceScope", "abcd") + child_kvp.setdefault("capabilities", {"iotEdge": True}) test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + 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(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[0], {}), ] @@ -275,21 +277,21 @@ def test_device_create_addchildren(self, sc_device_create_addchildren, req): 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") + assert ( + body["deviceScope"] == generate_parent_device().get("deviceScope") or + body["parentScopes"] == [generate_parent_device().get("deviceScope")] + ) - @pytest.fixture(params=[(200, 0), (200, 1)]) + @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) - nonedge_kvp = {} - if request.param[1] == 0: - nonedge_kvp.setdefault("capabilities", {"iotEdge": True}) - if request.param[1] == 1: - nonedge_kvp.setdefault("deviceScope", "abcd") + child_kvp = {} + child_kvp.setdefault("parentScopes", ["abcd"]) test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ) ] service_client.side_effect = test_side_effect @@ -2306,6 +2308,7 @@ def generate_parent_device(**kvp): "deviceId": device_id, "status": "disabled", "deviceScope": "ms-azure-iot-edge://{}-1234".format(device_id), + "parentScopes": ["ms-azure-iot-edge://{}-5678".format(device_id)], } for k in kvp: if payload.get(k): @@ -2332,13 +2335,13 @@ class TestEdgeOffline: @pytest.fixture(params=[(200, 200)]) def sc_getparent(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault( - "deviceScope", generate_parent_device().get("deviceScope") + child_kvp = {} + child_kvp.setdefault( + "parentScopes", [generate_parent_device().get("deviceScope")] ) test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[0], generate_parent_device()), ] @@ -2358,14 +2361,14 @@ def test_device_get_parent(self, sc_getparent): @pytest.fixture(params=[(200, 0), (200, 1)]) def sc_invalid_args_getparent(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} + child_kvp = {} if request.param[1] == 0: - nonedge_kvp.setdefault("capabilities", {"iotEdge": True}) + child_kvp.setdefault("parentScopes", []) if request.param[1] == 1: - nonedge_kvp.setdefault("deviceId", "") + child_kvp.setdefault("deviceId", "") test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ) ] service_client.side_effect = test_side_effect @@ -2384,13 +2387,13 @@ def test_device_getparent_error(self, serviceclient_generic_error): @pytest.fixture(params=[(200, 0), (200, 1)]) def sc_setparent(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} + child_kvp = {} if request.param[1] == 1: - nonedge_kvp.setdefault("deviceScope", "abcd") + child_kvp.setdefault("parentScopes", ["abcd"]) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[0], {}), ] @@ -2409,23 +2412,21 @@ def test_device_set_parent(self, sc_setparent): assert body["deviceId"] == child_device_id assert body["deviceScope"] == generate_parent_device().get("deviceScope") - @pytest.fixture(params=[(200, 0), (200, 1), (200, 2)]) + @pytest.fixture(params=[(200, 0), (200, 1)]) def sc_invalid_args_setparent(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - edge_kvp = {} - nonedge_kvp = {} + parent_kvp = {} + child_kvp = {} if request.param[1] == 0: - edge_kvp.setdefault("capabilities", {"iotEdge": False}) + parent_kvp.setdefault("capabilities", {"iotEdge": False}) if request.param[1] == 1: - nonedge_kvp.setdefault("capabilities", {"iotEdge": True}) - if request.param[1] == 2: - nonedge_kvp.setdefault("deviceScope", "abcd") + child_kvp.setdefault("parentScopes", ["abcd"]) test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_parent_device(**edge_kvp) + mocker, request.param[0], generate_parent_device(**parent_kvp) ), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), ] service_client.side_effect = test_side_effect @@ -2458,12 +2459,12 @@ def test_device_setparent_error(self, sc_setparent_error): @pytest.fixture(params=[(200, 200)]) def sc_invalid_etag_setparent(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault("etag", None) + child_kvp = {} + child_kvp.setdefault("etag", None) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), ] service_client.side_effect = test_side_effect @@ -2477,16 +2478,18 @@ def test_device_setparent_invalid_etag(self, sc_invalid_etag_setparent, exp): ) # add-children - @pytest.fixture(params=[(200, 0), (200, 1)]) + @pytest.fixture(params=[(200, 0), (200, 1), (200, 2)]) def sc_addchildren(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} + child_kvp = {} + if request.param[1] == 1: + child_kvp.setdefault("parentScopes", ["abcd"]) if request.param[1] == 1: - nonedge_kvp.setdefault("deviceScope", "abcd") + child_kvp.setdefault("capabilities", {"iotEdge": True}) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[0], {}), ] @@ -2503,25 +2506,26 @@ def test_device_children_add(self, sc_addchildren): 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") + assert ( + body["deviceScope"] == generate_parent_device().get("deviceScope") or + body["parentScopes"] == [generate_parent_device().get("deviceScope")] + ) - @pytest.fixture(params=[(200, 0), (200, 1), (200, 2)]) + @pytest.fixture(params=[(200, 0), (200, 1)]) def sc_invalid_args_addchildren(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - edge_kvp = {} - nonedge_kvp = {} + parent_kvp = {} + child_kvp = {} if request.param[1] == 0: - edge_kvp.setdefault("capabilities", {"iotEdge": False}) + parent_kvp.setdefault("capabilities", {"iotEdge": False}) if request.param[1] == 1: - nonedge_kvp.setdefault("capabilities", {"iotEdge": True}) - if request.param[1] == 2: - nonedge_kvp.setdefault("deviceScope", "abcd") + child_kvp.setdefault("parentScopes", ["abcd"]) test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_parent_device(**edge_kvp) + mocker, request.param[0], generate_parent_device(**parent_kvp) ), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), ] service_client.side_effect = test_side_effect @@ -2554,12 +2558,12 @@ def test_device_addchildren_error(self, sc_addchildren_error): @pytest.fixture(params=[(200, 200)]) def sc_invalid_etag_addchildren(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault("etag", None) + child_kvp = {} + child_kvp.setdefault("etag", None) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), ] service_client.side_effect = test_side_effect @@ -2576,12 +2580,12 @@ def test_device_addchildren_invalid_etag(self, sc_invalid_etag_setparent, exp): @pytest.fixture def sc_listchildren(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault( - "deviceScope", generate_parent_device().get("deviceScope") + child_kvp = {} + child_kvp.setdefault( + "parentScopes", [generate_parent_device().get("deviceScope")] ) result = [] - result.append(generate_child_device(**nonedge_kvp)) + result.append(generate_child_device(**child_kvp)) test_side_effect = [ build_mock_response(mocker, 200, generate_parent_device()), build_mock_response( @@ -2599,17 +2603,17 @@ def test_device_children_list(self, sc_listchildren): url = args[0][0].url assert "{}/devices/query?".format(mock_target["entity"]) in url assert args[0][0].method == "POST" - assert result == child_device_id + assert result == [child_device_id] @pytest.fixture(params=[(200, 0), (200, 1)]) def sc_invalid_args_listchildren(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - edge_kvp = {} + parent_kvp = {} if request.param[1] == 0: - edge_kvp.setdefault("capabilities", {"iotEdge": False}) + parent_kvp.setdefault("capabilities", {"iotEdge": False}) test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_parent_device(**edge_kvp) + mocker, request.param[0], generate_parent_device(**parent_kvp) ), build_mock_response( mocker, request.param[0], [], {"x-ms-continuation": None} @@ -2618,13 +2622,6 @@ def sc_invalid_args_listchildren(self, mocker, fixture_ghcs, fixture_sas, reques service_client.side_effect = test_side_effect return service_client - @pytest.mark.parametrize("exp", [CLIError]) - def test_device_listchildren_invalid_args(self, sc_invalid_args_listchildren, exp): - with pytest.raises(exp): - subject.iot_device_children_list( - fixture_cmd, device_id, mock_target["entity"] - ) - def test_device_listchildren_error(self, serviceclient_generic_error): with pytest.raises(CLIError): subject.iot_device_children_list( @@ -2635,14 +2632,14 @@ def test_device_listchildren_error(self, serviceclient_generic_error): @pytest.fixture(params=[(200, 200)]) def sc_removechildrenlist(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault( - "deviceScope", generate_parent_device().get("deviceScope") + child_kvp = {} + child_kvp.setdefault( + "parentScopes", [generate_parent_device().get("deviceScope")] ) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[0], {}), ] @@ -2663,22 +2660,22 @@ def sc_invalid_args_removechildrenlist( self, mocker, fixture_ghcs, fixture_sas, request ): service_client = mocker.patch(path_service_client) - edge_kvp = {} - nonedge_kvp = {} + parent_kvp = {} + child_kvp = {} if request.param[1] == 0: - edge_kvp.setdefault("capabilities", {"iotEdge": False}) - if request.param[1] == 1: - nonedge_kvp.setdefault("capabilities", {"iotEdge": True}) + parent_kvp.setdefault("capabilities", {"iotEdge": False}) + if request.param[1] == 2: + child_kvp.setdefault("parentScopes", [""]) if request.param[1] == 2: - nonedge_kvp.setdefault("deviceScope", "") + child_kvp.setdefault("deviceScope", "") if request.param[1] == 3: - nonedge_kvp.setdefault("deviceScope", "other") + child_kvp.setdefault("deviceScope", "other") test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_parent_device(**edge_kvp) + mocker, request.param[0], generate_parent_device(**parent_kvp) ), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), ] service_client.side_effect = test_side_effect @@ -2698,15 +2695,15 @@ def sc_invalid_etag_removechildrenlist( self, mocker, fixture_ghcs, fixture_sas, request ): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault( - "deviceScope", generate_parent_device().get("deviceScope") + child_kvp = {} + child_kvp.setdefault( + "parentScopes", [generate_parent_device().get("deviceScope")] ) - nonedge_kvp.setdefault("etag", None) + child_kvp.setdefault("etag", None) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[0], {}), ] @@ -2725,14 +2722,14 @@ def test_device_removechildrenlist_invalid_etag( @pytest.fixture(params=[(200, 400), (200, 401), (200, 500)]) def sc_removechildrenlist_error(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault( - "deviceScope", generate_parent_device().get("deviceScope") + child_kvp = {} + child_kvp.setdefault( + "parentScopes", [generate_parent_device().get("deviceScope")] ) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[1], {}), ] @@ -2748,19 +2745,19 @@ def test_device_removechildrenlist_error(self, sc_removechildrenlist_error): @pytest.fixture(params=[(200, 200)]) def sc_removechildrenall(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault( - "deviceScope", generate_parent_device().get("deviceScope") + child_kvp = {} + child_kvp.setdefault( + "parentScopes", [generate_parent_device().get("deviceScope")] ) result = [] - result.append(generate_child_device(**nonedge_kvp)) + result.append(generate_child_device(**child_kvp)) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( mocker, request.param[0], result, {"x-ms-continuation": None} ), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[0], {}), ] @@ -2781,20 +2778,20 @@ def sc_invalid_args_removechildrenall( self, mocker, fixture_ghcs, fixture_sas, request ): service_client = mocker.patch(path_service_client) - edge_kvp = {} - nonedge_kvp = {} - nonedge_kvp.setdefault( - "deviceScope", generate_parent_device().get("deviceScope") + parent_kvp = {} + child_kvp = {} + child_kvp.setdefault( + "parentScopes", [generate_parent_device().get("deviceScope")] ) result = [] - result.append(generate_child_device(**nonedge_kvp)) + result.append(generate_child_device(**child_kvp)) if request.param[1] == 0: - edge_kvp.setdefault("capabilities", {"iotEdge": False}) + parent_kvp.setdefault("capabilities", {"iotEdge": False}) if request.param[1] == 1: result = [] test_side_effect = [ build_mock_response( - mocker, request.param[0], generate_parent_device(**edge_kvp) + mocker, request.param[0], generate_parent_device(**parent_kvp) ), build_mock_response( mocker, request.param[0], result, {"x-ms-continuation": None} @@ -2817,20 +2814,20 @@ def sc_invalid_etag_removechildrenall( self, mocker, fixture_ghcs, fixture_sas, request ): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault( - "deviceScope", generate_parent_device().get("deviceScope") + child_kvp = {} + child_kvp.setdefault( + "parentScopes", [generate_parent_device().get("deviceScope")] ) - nonedge_kvp.setdefault("etag", None) + child_kvp.setdefault("etag", None) result = [] - result.append(generate_child_device(**nonedge_kvp)) + result.append(generate_child_device(**child_kvp)) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( mocker, request.param[0], result, {"x-ms-continuation": None} ), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[0], {}), ] @@ -2849,19 +2846,19 @@ def test_device_removechildrenall_invalid_etag( @pytest.fixture(params=[(200, 400), (200, 401), (200, 500)]) def sc_removechildrenall_error(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - nonedge_kvp = {} - nonedge_kvp.setdefault( - "deviceScope", generate_parent_device().get("deviceScope") + child_kvp = {} + child_kvp.setdefault( + "parentScopes", [generate_parent_device().get("deviceScope")] ) result = [] - result.append(generate_child_device(**nonedge_kvp)) + result.append(generate_child_device(**child_kvp)) test_side_effect = [ build_mock_response(mocker, request.param[0], generate_parent_device()), build_mock_response( mocker, request.param[0], result, {"x-ms-continuation": None} ), build_mock_response( - mocker, request.param[0], generate_child_device(**nonedge_kvp) + mocker, request.param[0], generate_child_device(**child_kvp) ), build_mock_response(mocker, request.param[1], {}), ] From 9f77b9a5efa409b119d2908cd6d0c0bafa1e0de9 Mon Sep 17 00:00:00 2001 From: YingXue Date: Tue, 10 Nov 2020 09:21:03 -0800 Subject: [PATCH 143/179] fix generate auth key (#270) --- azext_iot/operations/hub.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 6ab078378..2fc44c360 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -379,6 +379,29 @@ def iot_device_delete( raise CLIError(err) +def _update_device_key(target, device, auth_method, pk, sk): + resolver = SdkResolver(target=target) + service_sdk = resolver.get_sdk(SdkType.service_sdk) + + try: + auth = _assemble_auth(auth_method, pk, sk) + device["authentication"] = auth + etag = device.get("etag", None) + if etag: + headers = {} + headers["If-Match"] = '"{}"'.format(etag) + return service_sdk.devices.create_or_update_identity( + id=device["deviceId"], + device=device, + custom_headers=headers, + ) + raise LookupError("device etag not found.") + except CloudError as e: + raise CLIError(unpack_msrest_error(e)) + except LookupError as err: + raise CLIError(err) + + def iot_device_key_regenerate(cmd, hub_name, device_id, regenerate_key, resource_group_name=None, login=None): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -399,18 +422,7 @@ def iot_device_key_regenerate(cmd, hub_name, device_id, regenerate_key, resource pk = sk sk = temp - updated_device = _assemble_device( - device["deviceId"], - device["authentication"]["type"], - device["capabilities"]["iotEdge"], - pk, - sk, - device["status"].lower(), - device["statusReason"], - device.get("deviceScope", None) - ) - updated_device.etag = device["etag"] - return _iot_device_update(target, device_id, updated_device) + return _update_device_key(target, device, device["authentication"]["type"], pk, sk) def iot_device_get_parent( From 89e0fb99821d367b3a6ff558f53127b026b230e8 Mon Sep 17 00:00:00 2001 From: YingXue Date: Tue, 10 Nov 2020 10:23:49 -0800 Subject: [PATCH 144/179] fix device update (#271) --- azext_iot/operations/hub.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 2fc44c360..ce0ecfd55 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -170,6 +170,7 @@ def iot_device_create( try: device = _assemble_device( + False, device_id, auth_method, edge_enabled, @@ -194,6 +195,7 @@ def iot_device_create( def _assemble_device( + is_update, device_id, auth_method, edge_enabled, @@ -207,6 +209,16 @@ def _assemble_device( auth = _assemble_auth(auth_method, pk, sk) cap = DeviceCapabilities(iot_edge=edge_enabled) + if is_update: + device = Device( + device_id=device_id, + authentication=auth, + capabilities=cap, + status=status, + status_reason=status_reason, + device_scope=device_scope, + ) + return device if edge_enabled: parent_scope = [] if device_scope: @@ -324,12 +336,15 @@ def iot_device_update( auth, pk, sk = _parse_auth(parameters) updated_device = _assemble_device( + True, parameters['deviceId'], - auth, parameters['capabilities']['iotEdge'], + auth, + parameters['capabilities']['iotEdge'], pk, sk, parameters['status'].lower(), - parameters.get('statusReason') + parameters.get('statusReason'), + parameters.get('deviceScope') ) updated_device.etag = parameters.get("etag", "*") return _iot_device_update(target, device_id, updated_device) From 9152676b4969a9be40a87f6014b14758811cbbf7 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 10 Nov 2020 15:12:25 -0800 Subject: [PATCH 145/179] Increase sleep duration in ADT tests. --- azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py | 2 +- azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py | 2 +- azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py index cc8921118..a1faa580f 100644 --- a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py @@ -45,7 +45,7 @@ def test_dt_models(self): ) # Wait for RBAC to catch-up - sleep(15) + sleep(20) create_models_output = self.cmd( "dt model create -n {} --from-directory '{}'".format( 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 4c834cc86..02cd2da5c 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -246,7 +246,7 @@ def test_dt_endpoints_routes(self): ) ) - sleep(10) # Wait for service to catch-up + sleep(20) # Wait for service to catch-up list_ep_output = self.cmd( "dt endpoint list -n {}".format(endpoints_instance_name) 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 c63c89291..5c48e5979 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -42,7 +42,7 @@ def test_dt_twin(self): ) ) # Wait for RBAC to catch-up - sleep(15) + sleep(20) self.cmd( "dt model create -n {} --from-directory '{}'".format( From c89b4430e163365c4b37e7ad751e182b97d0c539 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 10 Nov 2020 15:43:54 -0800 Subject: [PATCH 146/179] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 03545931f..3be36b881 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ The **Azure IoT extension for Azure CLI** aims to accelerate the development, ma 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. +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: ``` ... From 3024ff8107713ccd242fe52816cdddc0e82af696 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Tue, 10 Nov 2020 19:42:41 -0800 Subject: [PATCH 147/179] Fix CI. --- dev_requirements | 1 + 1 file changed, 1 insertion(+) diff --git a/dev_requirements b/dev_requirements index 02e41c0dc..25ac53c05 100644 --- a/dev_requirements +++ b/dev_requirements @@ -5,6 +5,7 @@ pytest-env uamqp~=1.2 mock;python_version<'3.3' responses +urllib3[secure]>=1.21.1,<=1.25 black;python_version>='3.6' wheel==0.30.0 pre-commit From cdbc13ffdbba46b641bc93db2ae724908eb85973 Mon Sep 17 00:00:00 2001 From: Sapan Saxena <31940305+anusapan@users.noreply.github.com> Date: Thu, 12 Nov 2020 12:50:39 -0800 Subject: [PATCH 148/179] Add new examples of inline json input (#272) --- azext_iot/_help.py | 37 +++++++++++++++++++++++++++++-------- azext_iot/iothub/_help.py | 5 +++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index ec7d000e8..3040dc9a5 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -643,6 +643,11 @@ ] = """ type: command short-summary: Invoke an Edge module method. + examples: + - name: Invoke a direct method on edge device using a module from the cloud. + text: > + az iot hub invoke-module-method -n {iothub_name} -d {device_id} + -m '$edgeAgent' --method-name 'RestartModule' --method-payload '{"schemaVersion": "1.0"}' """ helps[ @@ -650,6 +655,11 @@ ] = """ type: command short-summary: Invoke a device method. + examples: + - name: Invoke a direct method on device from the cloud. + text: > + az iot hub invoke-device-method --hub-name {iothub_name} --device-id {device_id} + --method-name Reboot --method-payload '{"version":"1.0"}' """ helps[ @@ -1134,14 +1144,15 @@ - name: Create an enrollment '{enrollment_id}' with attestation type 'x509' in the Azure IoT Device Provisioning Service '{dps_name}' in the resource group '{resource_group_name}' with provisioning status 'disabled', target IoT Hub - '{iothub_host_name}', device id '{device_id}' and initial twin - properties '{"location":{"region":"US"}}'. + '{iothub_host_name}', device id '{device_id}', initial twin properties + '{"location":{"region":"US"}}' and initial twin tags '{"version":"1"}'. text: > az iot dps enrollment create -g {resource_group_name} --dps-name {dps_name} --enrollment-id {enrollment_id} --attestation-type x509 --certificate-path /certificates/Certificate.pem --provisioning-status disabled --iot-hub-host-name {iothub_host_name} - --initial-twin-properties "{'location':{'region':'US'}}" --device-id {device_id} + --initial-twin-properties "{'location':{'region':'US'}}" + --initial-twin-tags "{'version':'1'}" --device-id {device_id} - name: Create an enrollment 'MyEnrollment' with attestation type 'tpm' in the Azure IoT Device Provisioning Service '{dps_name}' in the resource group '{resource_group_name}'. text: > @@ -1215,6 +1226,13 @@ az iot dps enrollment update -g {resource_group_name} --dps-name {dps_name} --enrollment-id {enrollment_id} --allocation-policy geolatency --etag AAAAAAAAAAA= --iot-hubs "{iot_hub_host_name1} {iot_hub_host_name2} {iot_hub_host_name3}" + - name: Update enrollment '{enrollment_id}' in the Azure IoT Device Provisioning Service '{dps_name}' + in the resource group '{resource_group_name}' with + initial twin properties '{"location":{"region":"USA"}}' and initial twin tags '{"version":"2"}'. + text: > + az iot dps enrollment update -g {resource_group_name} --dps-name {dps_name} + --enrollment-id {enrollment_id} --initial-twin-properties "{'location':{'region':'USA'}}" + --initial-twin-tags "{'version1':'2'}" """ helps[ @@ -1271,13 +1289,15 @@ --enrollment-id {enrollment_id} --secondary-ca-name {certificate_name} - name: Create an enrollment group '{enrollment_id}' in the Azure IoT provisioning service 'MyDps' in the resource group '{resource_group_name}' with provisioning status - 'enabled', target IoT Hub '{iothub_host_name}' and initial twin - tags '{"location":{"region":"US"}} using an intermediate certificate as primary certificate'. + 'enabled', target IoT Hub '{iothub_host_name}', initial twin properties + '{"location":{"region":"US"}}' and initial twin tags '{"version_dps":"1"}' + using an intermediate certificate as primary certificate'. text: > az iot dps enrollment-group create -g {resource_group_name} --dps-name {dps_name} --enrollment-id {enrollment_id} --certificate-path /certificates/Certificate.pem --provisioning-status enabled --iot-hub-host-name {iothub_host_name} - --initial-twin-tags "{'location':{'region':'US'}}" + --initial-twin-properties "{'location':{'region':'US'}}" + --initial-twin-tags "{'version_dps':'1'}" - name: Create an enrollment group '{enrollment_id}' in the Azure IoT provisioning service '{dps_name}' in the resource group '{resource_group_name} with attestation type 'symmetrickey'. text: > @@ -1298,10 +1318,11 @@ short-summary: Update an enrollment group in an Azure IoT Hub Device Provisioning Service. examples: - name: Update enrollment group '{enrollment_id}' in the Azure IoT provisioning service '{dps_name}' - in the resource group '{resource_group_name}' with new initial twin tags. + in the resource group '{resource_group_name}' with initial twin properties and initial twin tags. text: > az iot dps enrollment-group update -g {resource_group_name} --dps-name {dps_name} - --enrollment-id {enrollment_id} --initial-twin-tags "{'location':{'region':'US2'}}" --etag AAAAAAAAAAA= + --enrollment-id {enrollment_id} --initial-twin-properties "{'location':{'region':'USA'}}" + --initial-twin-tags "{'version_dps':'2'}" --etag AAAAAAAAAAA= - name: Update enrollment group '{enrollment_id}' in the Azure IoT provisioning service '{dps_name}' in the resource group '{resource_group_name}' with new primary intermediate certificate and remove existing secondary intermediate certificate. diff --git a/azext_iot/iothub/_help.py b/azext_iot/iothub/_help.py index 4249e73a9..a23ee344c 100644 --- a/azext_iot/iothub/_help.py +++ b/azext_iot/iothub/_help.py @@ -43,6 +43,11 @@ def load_iothub_help(): text: > az iot hub job create --job-id {job_name} --job-type scheduleDeviceMethod -n {iothub_name} --method-name setSyncIntervalSec --method-payload 30 --query-condition "properties.reported.settings.syncIntervalSec != 30" + + - name: Create and schedule a job to invoke a device method for all devices. + text: > + az iot hub job create --job-id {job_name} --job-type scheduleDeviceMethod -q "*" -n {iothub_name} + --method-name setSyncIntervalSec --method-payload '{"version":"1.0"}' """ helps["iot hub job show"] = """ From 020f2fe14878512b9af5577399f70e22165ce4e7 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 12 Nov 2020 16:40:54 -0800 Subject: [PATCH 149/179] Update create-release.yml for Azure Pipelines (#273) --- .azure-devops/create-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index be446801a..a46fe590a 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -96,7 +96,7 @@ jobs: - task: GitHubRelease@0 inputs: - gitHubConnection: AzIoTCLIGitHub + gitHubConnection: aziotcli repositoryName: $(Build.Repository.Name) action: 'create' target: '$(Build.SourceVersion)' From 13eb8660a6159482b6eb0ae8adc0fa2dbcfb8ca7 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 12 Nov 2020 16:44:16 -0800 Subject: [PATCH 150/179] Update create-release.yml --- .azure-devops/create-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index a46fe590a..be446801a 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -96,7 +96,7 @@ jobs: - task: GitHubRelease@0 inputs: - gitHubConnection: aziotcli + gitHubConnection: AzIoTCLIGitHub repositoryName: $(Build.Repository.Name) action: 'create' target: '$(Build.SourceVersion)' From 0b4468d4affa22a2d2808f4bca251fcdfa5895b1 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 16 Nov 2020 14:54:12 -0800 Subject: [PATCH 151/179] Remove regenerate-key introduce regenerate-keys. (#276) * Remove regenerate-key introduce regenerate-keys. * Fix linter issues. --- HISTORY.rst | 7 ++++ azext_iot/_help.py | 12 +++---- azext_iot/_params.py | 11 +++---- azext_iot/commands.py | 2 +- azext_iot/common/shared.py | 10 ------ azext_iot/common/utility.py | 9 ------ azext_iot/constants.py | 2 +- azext_iot/operations/hub.py | 43 +++++++++++-------------- azext_iot/tests/test_iot_ext_int.py | 17 ++++------ azext_iot/tests/test_iot_ext_unit.py | 48 ++++++++++++---------------- 10 files changed, 66 insertions(+), 95 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a89bc382c..0563cd12d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,13 @@ Release History =============== +0.10.7 ++++++++++++++++ + +**IoT Hub updates** + +* Breaking Change - removes and replaces "az iot hub device-identity regenerate-key" with the new "az iot hub device-identity regenerate-keys". + 0.10.6 +++++++++++++++ diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 3040dc9a5..b04adf2ae 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -216,15 +216,15 @@ """ helps[ - "iot hub device-identity regenerate-key" + "iot hub device-identity regenerate-keys" ] = """ type: command - short-summary: Regenerate target keys of an IoT Hub device with sas authentication. + short-summary: Regenerate keys of an IoT Hub device with symmetric key authentication. examples: - - name: Regenerate the primary key. - text: az iot hub device-identity regenerate-key -d {device_id} -n {iothub_name} --kt primary - - name: Swap the primary and secondary keys. - text: az iot hub device-identity regenerate-key -d {device_id} -n {iothub_name} --kt swap + - name: Regenerates the primary and secondary keys. + text: az iot hub device-identity regenerate-keys -d {device_id} -n {iothub_name} + - name: Swaps the primary and secondary keys. No key generation occurs. + text: az iot hub device-identity regenerate-keys -d {device_id} -n {iothub_name} --swap """ helps[ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 8bb253ed7..8d6a15b3a 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -31,7 +31,6 @@ JobCreateType, JobStatusType, AuthenticationType, - RegenerateKeyType, ) from azext_iot._validators import mode2_iot_login_handler from azext_iot.assets.user_messages import info_param_properties_device @@ -416,12 +415,12 @@ def load_arguments(self, _): deprecate_info=context.deprecate() ) - with self.argument_context('iot hub device-identity regenerate-key') as context: + with self.argument_context('iot hub device-identity regenerate-keys') as context: context.argument( - "regenerate_key", - options_list=["--key-type", "--kt"], - arg_type=get_enum_type(RegenerateKeyType), - help="Target key type to regenerate." + "swap", + options_list=["--swap"], + arg_type=get_three_state_flag(), + help="Flag indicating the operation should swap primary and secondary keys only. No new keys will be generated. ", ) with self.argument_context("iot hub device-identity export") as context: diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 1fe5317d5..9a6302ad2 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -40,7 +40,7 @@ def load_command_table(self, _): setter_name="iot_device_update", custom_func_name="update_iot_device_custom" ) - cmd_group.command("regenerate-key", 'iot_device_key_regenerate') + cmd_group.command("regenerate-keys", "iot_device_keys_regenerate") cmd_group.command( "show-connection-string", "iot_get_device_connection_string", diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index 6440659cd..56024deb0 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -216,16 +216,6 @@ class AuthenticationType(Enum): identityBased = "identity" -class RegenerateKeyType(Enum): - """ - Target key type for regeneration. - """ - - primary = KeyType.primary.value - secondary = KeyType.secondary.value - swap = "swap" - - class IoTHubStateType(Enum): """ IoT Hub State Property diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index 954eec775..a939d5855 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -18,7 +18,6 @@ import re import hmac import hashlib -import random from threading import Event, Thread from datetime import datetime @@ -516,11 +515,3 @@ def compute_device_key(primary_key, registration_id): ).digest() ) return device_key - - -def generate_key(byte_length=32): - key = "" - while byte_length > 0: - key += chr(random.randrange(1, 128)) - byte_length -= 1 - return base64.b64encode(key.encode()).decode("utf-8") diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 428b4ece9..239b7b3e0 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.6" +VERSION = "0.10.7" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index ce0ecfd55..7256dd7d9 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -24,7 +24,6 @@ ConfigType, KeyType, SettleType, - RegenerateKeyType, IoTHubStateType ) from azext_iot.iothub.providers.discovery import IotHubDiscovery @@ -37,7 +36,6 @@ init_monitoring, process_json_arg, ensure_min_version, - generate_key ) from azext_iot._factory import SdkResolver, CloudError from azext_iot.operations.generic import _execute_query, _process_top @@ -401,43 +399,38 @@ def _update_device_key(target, device, auth_method, pk, sk): try: auth = _assemble_auth(auth_method, pk, sk) device["authentication"] = auth - etag = device.get("etag", None) - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.devices.create_or_update_identity( - id=device["deviceId"], - device=device, - custom_headers=headers, - ) - raise LookupError("device etag not found.") + etag = device.get("etag", "*") + headers = {} + headers["If-Match"] = '"{}"'.format(etag) + return service_sdk.devices.create_or_update_identity( + id=device["deviceId"], + device=device, + custom_headers=headers, + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) -def iot_device_key_regenerate(cmd, hub_name, device_id, regenerate_key, resource_group_name=None, login=None): +def iot_device_keys_regenerate(cmd, hub_name, device_id, swap=False, 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 ) device = _iot_device_show(target, device_id) if (device["authentication"]["type"] != "sas"): - raise CLIError("Device authentication should be of type sas") - - pk = device["authentication"]["symmetricKey"]["primaryKey"] - sk = device["authentication"]["symmetricKey"]["secondaryKey"] - if regenerate_key == RegenerateKeyType.primary.value: - pk = generate_key() - if regenerate_key == RegenerateKeyType.secondary.value: - sk = generate_key() - if regenerate_key == RegenerateKeyType.swap.value: + raise CLIError("Device authentication must be of type sas (symmetricKey).") + + if swap: + pk = device["authentication"]["symmetricKey"]["primaryKey"] + sk = device["authentication"]["symmetricKey"]["secondaryKey"] + temp = pk pk = sk sk = temp - return _update_device_key(target, device, device["authentication"]["type"], pk, sk) + return _update_device_key(target, device, device["authentication"]["type"], pk, sk) + + return _update_device_key(target, device, device["authentication"]["type"], "", "") def iot_device_get_parent( diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 93cfbeb39..d014eb8ac 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -518,10 +518,9 @@ def test_hub_devices(self): ], ) - # Test 'az iot hub device regenerate-key' + # Test 'az iot hub device regenerate-keys' device = self.cmd( - '''iot hub device-identity regenerate-key -d {} -n {} -g {} --kt primary - '''.format( + '''iot hub device-identity regenerate-keys -d {} -n {} -g {}'''.format( edge_device_ids[1], LIVE_HUB, LIVE_RG ), checks=[ @@ -529,10 +528,9 @@ def test_hub_devices(self): ] ).get_output_in_json() - # Test swap keys 'az iot hub device regenerate-key' + # Test swap keys 'az iot hub device regenerate-keys' self.cmd( - '''iot hub device-identity regenerate-key -d {} -n {} -g {} --kt swap - '''.format( + '''iot hub device-identity regenerate-keys -d {} -n {} -g {} --swap'''.format( edge_device_ids[1], LIVE_HUB, LIVE_RG ), checks=[ @@ -541,10 +539,9 @@ def test_hub_devices(self): ], ) - # Test 'az iot hub device regenerate-key' with non sas authentication - self.cmd("iot hub device-identity regenerate-key -d {} -n {} -g {} --kt secondary" - .format(device_ids[0], LIVE_HUB, LIVE_RG), - expect_failure=True) + # Test 'az iot hub device regenerate-keys' with non sas authentication + self.cmd("iot hub device-identity regenerate-keys -d {} -n {} -g {}".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] diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index 02555b21d..cf362dcb2 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -640,7 +640,7 @@ def test_device_update_error(self, serviceclient_generic_error, req): ) -class TestDeviceRegenerateKey: +class TestDeviceRegenerateKeys: @pytest.fixture(params=[200]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) @@ -662,10 +662,10 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client.side_effect = test_side_effect return service_client - @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) - def test_device_key_regenerate(self, fixture_cmd, serviceclient, req): - subject.iot_device_key_regenerate( - fixture_cmd, mock_target["entity"], device_id, req + @pytest.mark.parametrize("swap", [False, True]) + def test_device_keys_regenerate(self, fixture_cmd, serviceclient, swap): + subject.iot_device_keys_regenerate( + fixture_cmd, mock_target["entity"], device_id, swap=swap ) args = serviceclient.call_args assert ( @@ -674,16 +674,17 @@ def test_device_key_regenerate(self, fixture_cmd, serviceclient, req): assert args[0][0].method == "PUT" body = json.loads(args[0][0].body) - if(req == "primary"): - assert body["authentication"]["symmetricKey"]["primaryKey"] != "123" - if(req == "secondary"): - assert body["authentication"]["symmetricKey"]["secondaryKey"] != "321" - if(req == "swap"): + + if(swap): assert body["authentication"]["symmetricKey"]["primaryKey"] == "321" assert body["authentication"]["symmetricKey"]["secondaryKey"] == "123" + return + + assert body["authentication"]["symmetricKey"]["primaryKey"] == "" + assert body["authentication"]["symmetricKey"]["secondaryKey"] == "" @pytest.fixture(params=[200]) - def serviceclient_invalid_args(self, mocker, fixture_ghcs, fixture_sas, request): + def serviceclient_invalid_auth(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) kvp = {} kvp.setdefault("authentication", {"type": "test"}) @@ -693,25 +694,18 @@ def serviceclient_invalid_args(self, mocker, fixture_ghcs, fixture_sas, request) service_client.side_effect = test_side_effect return service_client - @pytest.mark.parametrize( - "req, exp", - [ - ("primary", CLIError), - ("secondary", CLIError), - ("swap", CLIError) - ] - ) - def test_device_key_regenerate_invalid_args(self, fixture_cmd, serviceclient_invalid_args, req, exp): - with pytest.raises(exp): - subject.iot_device_key_regenerate( - fixture_cmd, mock_target["entity"], device_id, req + @pytest.mark.parametrize("swap", [False, True]) + def test_device_keys_regenerate_invalid_auth(self, serviceclient_invalid_auth, swap): + with pytest.raises(CLIError): + subject.iot_device_keys_regenerate( + fixture_cmd, mock_target["entity"], device_id, swap=swap ) - @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) - def test_device_key_regenerate_error(self, serviceclient_generic_error, req): + @pytest.mark.parametrize("swap", [False, True]) + def test_device_keys_regenerate_error(self, serviceclient_generic_error, swap): with pytest.raises(CLIError): - subject.iot_device_key_regenerate( - fixture_cmd, mock_target["entity"], device_id, req + subject.iot_device_keys_regenerate( + fixture_cmd, mock_target["entity"], device_id, swap=swap ) From bda9764da60e5c992e2f9669355ee6723139ae8f Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 18 Nov 2020 14:34:37 -0800 Subject: [PATCH 152/179] Revert "Remove regenerate-key introduce regenerate-keys. (#276)" (#278) This reverts commit 0b4468d4affa22a2d2808f4bca251fcdfa5895b1. --- HISTORY.rst | 7 ---- azext_iot/_help.py | 12 +++---- azext_iot/_params.py | 11 ++++--- azext_iot/commands.py | 2 +- azext_iot/common/shared.py | 10 ++++++ azext_iot/common/utility.py | 9 ++++++ azext_iot/constants.py | 2 +- azext_iot/operations/hub.py | 43 ++++++++++++++----------- azext_iot/tests/test_iot_ext_int.py | 17 ++++++---- azext_iot/tests/test_iot_ext_unit.py | 48 ++++++++++++++++------------ 10 files changed, 95 insertions(+), 66 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0563cd12d..a89bc382c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,13 +2,6 @@ Release History =============== -0.10.7 -+++++++++++++++ - -**IoT Hub updates** - -* Breaking Change - removes and replaces "az iot hub device-identity regenerate-key" with the new "az iot hub device-identity regenerate-keys". - 0.10.6 +++++++++++++++ diff --git a/azext_iot/_help.py b/azext_iot/_help.py index b04adf2ae..3040dc9a5 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -216,15 +216,15 @@ """ helps[ - "iot hub device-identity regenerate-keys" + "iot hub device-identity regenerate-key" ] = """ type: command - short-summary: Regenerate keys of an IoT Hub device with symmetric key authentication. + short-summary: Regenerate target keys of an IoT Hub device with sas authentication. examples: - - name: Regenerates the primary and secondary keys. - text: az iot hub device-identity regenerate-keys -d {device_id} -n {iothub_name} - - name: Swaps the primary and secondary keys. No key generation occurs. - text: az iot hub device-identity regenerate-keys -d {device_id} -n {iothub_name} --swap + - name: Regenerate the primary key. + text: az iot hub device-identity regenerate-key -d {device_id} -n {iothub_name} --kt primary + - name: Swap the primary and secondary keys. + text: az iot hub device-identity regenerate-key -d {device_id} -n {iothub_name} --kt swap """ helps[ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 8d6a15b3a..8bb253ed7 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -31,6 +31,7 @@ JobCreateType, JobStatusType, AuthenticationType, + RegenerateKeyType, ) from azext_iot._validators import mode2_iot_login_handler from azext_iot.assets.user_messages import info_param_properties_device @@ -415,12 +416,12 @@ def load_arguments(self, _): deprecate_info=context.deprecate() ) - with self.argument_context('iot hub device-identity regenerate-keys') as context: + with self.argument_context('iot hub device-identity regenerate-key') as context: context.argument( - "swap", - options_list=["--swap"], - arg_type=get_three_state_flag(), - help="Flag indicating the operation should swap primary and secondary keys only. No new keys will be generated. ", + "regenerate_key", + options_list=["--key-type", "--kt"], + arg_type=get_enum_type(RegenerateKeyType), + help="Target key type to regenerate." ) with self.argument_context("iot hub device-identity export") as context: diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 9a6302ad2..1fe5317d5 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -40,7 +40,7 @@ def load_command_table(self, _): setter_name="iot_device_update", custom_func_name="update_iot_device_custom" ) - cmd_group.command("regenerate-keys", "iot_device_keys_regenerate") + cmd_group.command("regenerate-key", 'iot_device_key_regenerate') cmd_group.command( "show-connection-string", "iot_get_device_connection_string", diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index 56024deb0..6440659cd 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -216,6 +216,16 @@ class AuthenticationType(Enum): identityBased = "identity" +class RegenerateKeyType(Enum): + """ + Target key type for regeneration. + """ + + primary = KeyType.primary.value + secondary = KeyType.secondary.value + swap = "swap" + + class IoTHubStateType(Enum): """ IoT Hub State Property diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index a939d5855..954eec775 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -18,6 +18,7 @@ import re import hmac import hashlib +import random from threading import Event, Thread from datetime import datetime @@ -515,3 +516,11 @@ def compute_device_key(primary_key, registration_id): ).digest() ) return device_key + + +def generate_key(byte_length=32): + key = "" + while byte_length > 0: + key += chr(random.randrange(1, 128)) + byte_length -= 1 + return base64.b64encode(key.encode()).decode("utf-8") diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 239b7b3e0..428b4ece9 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.7" +VERSION = "0.10.6" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 7256dd7d9..ce0ecfd55 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -24,6 +24,7 @@ ConfigType, KeyType, SettleType, + RegenerateKeyType, IoTHubStateType ) from azext_iot.iothub.providers.discovery import IotHubDiscovery @@ -36,6 +37,7 @@ init_monitoring, process_json_arg, ensure_min_version, + generate_key ) from azext_iot._factory import SdkResolver, CloudError from azext_iot.operations.generic import _execute_query, _process_top @@ -399,38 +401,43 @@ def _update_device_key(target, device, auth_method, pk, sk): try: auth = _assemble_auth(auth_method, pk, sk) device["authentication"] = auth - etag = device.get("etag", "*") - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.devices.create_or_update_identity( - id=device["deviceId"], - device=device, - custom_headers=headers, - ) + etag = device.get("etag", None) + if etag: + headers = {} + headers["If-Match"] = '"{}"'.format(etag) + return service_sdk.devices.create_or_update_identity( + id=device["deviceId"], + device=device, + custom_headers=headers, + ) + raise LookupError("device etag not found.") except CloudError as e: raise CLIError(unpack_msrest_error(e)) + except LookupError as err: + raise CLIError(err) -def iot_device_keys_regenerate(cmd, hub_name, device_id, swap=False, resource_group_name=None, login=None): +def iot_device_key_regenerate(cmd, hub_name, device_id, regenerate_key, 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 ) device = _iot_device_show(target, device_id) if (device["authentication"]["type"] != "sas"): - raise CLIError("Device authentication must be of type sas (symmetricKey).") - - if swap: - pk = device["authentication"]["symmetricKey"]["primaryKey"] - sk = device["authentication"]["symmetricKey"]["secondaryKey"] - + raise CLIError("Device authentication should be of type sas") + + pk = device["authentication"]["symmetricKey"]["primaryKey"] + sk = device["authentication"]["symmetricKey"]["secondaryKey"] + if regenerate_key == RegenerateKeyType.primary.value: + pk = generate_key() + if regenerate_key == RegenerateKeyType.secondary.value: + sk = generate_key() + if regenerate_key == RegenerateKeyType.swap.value: temp = pk pk = sk sk = temp - return _update_device_key(target, device, device["authentication"]["type"], pk, sk) - - return _update_device_key(target, device, device["authentication"]["type"], "", "") + return _update_device_key(target, device, device["authentication"]["type"], pk, sk) def iot_device_get_parent( diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index d014eb8ac..93cfbeb39 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -518,9 +518,10 @@ def test_hub_devices(self): ], ) - # Test 'az iot hub device regenerate-keys' + # Test 'az iot hub device regenerate-key' device = self.cmd( - '''iot hub device-identity regenerate-keys -d {} -n {} -g {}'''.format( + '''iot hub device-identity regenerate-key -d {} -n {} -g {} --kt primary + '''.format( edge_device_ids[1], LIVE_HUB, LIVE_RG ), checks=[ @@ -528,9 +529,10 @@ def test_hub_devices(self): ] ).get_output_in_json() - # Test swap keys 'az iot hub device regenerate-keys' + # Test swap keys 'az iot hub device regenerate-key' self.cmd( - '''iot hub device-identity regenerate-keys -d {} -n {} -g {} --swap'''.format( + '''iot hub device-identity regenerate-key -d {} -n {} -g {} --kt swap + '''.format( edge_device_ids[1], LIVE_HUB, LIVE_RG ), checks=[ @@ -539,9 +541,10 @@ def test_hub_devices(self): ], ) - # Test 'az iot hub device regenerate-keys' with non sas authentication - self.cmd("iot hub device-identity regenerate-keys -d {} -n {} -g {}".format( - device_ids[0], LIVE_HUB, LIVE_RG), expect_failure=True) + # Test 'az iot hub device regenerate-key' with non sas authentication + self.cmd("iot hub device-identity regenerate-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] diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index cf362dcb2..02555b21d 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -640,7 +640,7 @@ def test_device_update_error(self, serviceclient_generic_error, req): ) -class TestDeviceRegenerateKeys: +class TestDeviceRegenerateKey: @pytest.fixture(params=[200]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) @@ -662,10 +662,10 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client.side_effect = test_side_effect return service_client - @pytest.mark.parametrize("swap", [False, True]) - def test_device_keys_regenerate(self, fixture_cmd, serviceclient, swap): - subject.iot_device_keys_regenerate( - fixture_cmd, mock_target["entity"], device_id, swap=swap + @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) + def test_device_key_regenerate(self, fixture_cmd, serviceclient, req): + subject.iot_device_key_regenerate( + fixture_cmd, mock_target["entity"], device_id, req ) args = serviceclient.call_args assert ( @@ -674,17 +674,16 @@ def test_device_keys_regenerate(self, fixture_cmd, serviceclient, swap): assert args[0][0].method == "PUT" body = json.loads(args[0][0].body) - - if(swap): + if(req == "primary"): + assert body["authentication"]["symmetricKey"]["primaryKey"] != "123" + if(req == "secondary"): + assert body["authentication"]["symmetricKey"]["secondaryKey"] != "321" + if(req == "swap"): assert body["authentication"]["symmetricKey"]["primaryKey"] == "321" assert body["authentication"]["symmetricKey"]["secondaryKey"] == "123" - return - - assert body["authentication"]["symmetricKey"]["primaryKey"] == "" - assert body["authentication"]["symmetricKey"]["secondaryKey"] == "" @pytest.fixture(params=[200]) - def serviceclient_invalid_auth(self, mocker, fixture_ghcs, fixture_sas, request): + def serviceclient_invalid_args(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) kvp = {} kvp.setdefault("authentication", {"type": "test"}) @@ -694,18 +693,25 @@ def serviceclient_invalid_auth(self, mocker, fixture_ghcs, fixture_sas, request) service_client.side_effect = test_side_effect return service_client - @pytest.mark.parametrize("swap", [False, True]) - def test_device_keys_regenerate_invalid_auth(self, serviceclient_invalid_auth, swap): - with pytest.raises(CLIError): - subject.iot_device_keys_regenerate( - fixture_cmd, mock_target["entity"], device_id, swap=swap + @pytest.mark.parametrize( + "req, exp", + [ + ("primary", CLIError), + ("secondary", CLIError), + ("swap", CLIError) + ] + ) + def test_device_key_regenerate_invalid_args(self, fixture_cmd, serviceclient_invalid_args, req, exp): + with pytest.raises(exp): + subject.iot_device_key_regenerate( + fixture_cmd, mock_target["entity"], device_id, req ) - @pytest.mark.parametrize("swap", [False, True]) - def test_device_keys_regenerate_error(self, serviceclient_generic_error, swap): + @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) + def test_device_key_regenerate_error(self, serviceclient_generic_error, req): with pytest.raises(CLIError): - subject.iot_device_keys_regenerate( - fixture_cmd, mock_target["entity"], device_id, swap=swap + subject.iot_device_key_regenerate( + fixture_cmd, mock_target["entity"], device_id, req ) From a5e3accbb036511e618114a988ce19f8af22c9db Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 18 Nov 2020 17:08:36 -0800 Subject: [PATCH 153/179] Better key gen (#279) * Generate cryptographicaly secure device key. * Increment version. --- HISTORY.rst | 10 ++++++++++ azext_iot/_help.py | 10 +++++----- azext_iot/_params.py | 8 ++++---- azext_iot/commands.py | 2 +- azext_iot/common/shared.py | 2 +- azext_iot/common/utility.py | 13 +++++++------ azext_iot/constants.py | 2 +- azext_iot/operations/hub.py | 11 ++++++----- azext_iot/tests/test_iot_ext_int.py | 12 ++++++------ 9 files changed, 41 insertions(+), 29 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a89bc382c..0d24273c0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,15 @@ Release History =============== + +0.10.7 ++++++++++++++++ + +**IoT Hub updates** + +* Change command name from az iot hub device-identity `regenerate-key` to `renew-key` to better align with az cli core verbs. + + 0.10.6 +++++++++++++++ @@ -24,6 +33,7 @@ Release History * 'az iot hub device-identity remove-children' is deprecated use 'az iot hub device-identity children remove' instead. Deprecated command is planned to be removed by December 2021 * 'az iot hub device-identity list-children' is deprecated use 'az iot hub device-identity children list' instead. Deprecated command group is planned to be removed by December 2021 + 0.10.5 +++++++++++++++ diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 3040dc9a5..7be342f37 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -216,15 +216,15 @@ """ helps[ - "iot hub device-identity regenerate-key" + "iot hub device-identity renew-key" ] = """ type: command - short-summary: Regenerate target keys of an IoT Hub device with sas authentication. + short-summary: Renew target keys of an IoT Hub device with sas authentication. examples: - - name: Regenerate the primary key. - text: az iot hub device-identity regenerate-key -d {device_id} -n {iothub_name} --kt primary + - name: Renew the primary key. + text: az iot hub device-identity renew-key -d {device_id} -n {iothub_name} --kt primary - name: Swap the primary and secondary keys. - text: az iot hub device-identity regenerate-key -d {device_id} -n {iothub_name} --kt swap + text: az iot hub device-identity renew-key -d {device_id} -n {iothub_name} --kt swap """ helps[ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 8bb253ed7..58839506e 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -31,7 +31,7 @@ JobCreateType, JobStatusType, AuthenticationType, - RegenerateKeyType, + RenewKeyType, ) from azext_iot._validators import mode2_iot_login_handler from azext_iot.assets.user_messages import info_param_properties_device @@ -416,11 +416,11 @@ def load_arguments(self, _): deprecate_info=context.deprecate() ) - with self.argument_context('iot hub device-identity regenerate-key') as context: + with self.argument_context('iot hub device-identity renew-key') as context: context.argument( - "regenerate_key", + "renew_key_type", options_list=["--key-type", "--kt"], - arg_type=get_enum_type(RegenerateKeyType), + arg_type=get_enum_type(RenewKeyType), help="Target key type to regenerate." ) diff --git a/azext_iot/commands.py b/azext_iot/commands.py index 1fe5317d5..68a1e021b 100644 --- a/azext_iot/commands.py +++ b/azext_iot/commands.py @@ -40,7 +40,7 @@ def load_command_table(self, _): setter_name="iot_device_update", custom_func_name="update_iot_device_custom" ) - cmd_group.command("regenerate-key", 'iot_device_key_regenerate') + cmd_group.command("renew-key", 'iot_device_key_regenerate') cmd_group.command( "show-connection-string", "iot_get_device_connection_string", diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index 6440659cd..3754e8c0d 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -216,7 +216,7 @@ class AuthenticationType(Enum): identityBased = "identity" -class RegenerateKeyType(Enum): +class RenewKeyType(Enum): """ Target key type for regeneration. """ diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index 954eec775..1e21672fe 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -18,7 +18,6 @@ import re import hmac import hashlib -import random from threading import Event, Thread from datetime import datetime @@ -519,8 +518,10 @@ def compute_device_key(primary_key, registration_id): def generate_key(byte_length=32): - key = "" - while byte_length > 0: - key += chr(random.randrange(1, 128)) - byte_length -= 1 - return base64.b64encode(key.encode()).decode("utf-8") + """ + Generate cryptographically secure device key. + """ + import secrets + + token_bytes = secrets.token_bytes(byte_length) + return base64.b64encode(token_bytes).decode("utf8") diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 428b4ece9..239b7b3e0 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.6" +VERSION = "0.10.7" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index ce0ecfd55..8e70e5bc9 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -24,7 +24,7 @@ ConfigType, KeyType, SettleType, - RegenerateKeyType, + RenewKeyType, IoTHubStateType ) from azext_iot.iothub.providers.discovery import IotHubDiscovery @@ -417,7 +417,7 @@ def _update_device_key(target, device, auth_method, pk, sk): raise CLIError(err) -def iot_device_key_regenerate(cmd, hub_name, device_id, regenerate_key, resource_group_name=None, login=None): +def iot_device_key_regenerate(cmd, hub_name, device_id, renew_key_type, 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 @@ -428,11 +428,12 @@ def iot_device_key_regenerate(cmd, hub_name, device_id, regenerate_key, resource pk = device["authentication"]["symmetricKey"]["primaryKey"] sk = device["authentication"]["symmetricKey"]["secondaryKey"] - if regenerate_key == RegenerateKeyType.primary.value: + + if renew_key_type == RenewKeyType.primary.value: pk = generate_key() - if regenerate_key == RegenerateKeyType.secondary.value: + if renew_key_type == RenewKeyType.secondary.value: sk = generate_key() - if regenerate_key == RegenerateKeyType.swap.value: + if renew_key_type == RenewKeyType.swap.value: temp = pk pk = sk sk = temp diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 93cfbeb39..b8b7cde90 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -518,9 +518,9 @@ def test_hub_devices(self): ], ) - # Test 'az iot hub device regenerate-key' + # Test 'az iot hub device renew-key' device = self.cmd( - '''iot hub device-identity regenerate-key -d {} -n {} -g {} --kt primary + '''iot hub device-identity renew-key -d {} -n {} -g {} --kt primary '''.format( edge_device_ids[1], LIVE_HUB, LIVE_RG ), @@ -529,9 +529,9 @@ def test_hub_devices(self): ] ).get_output_in_json() - # Test swap keys 'az iot hub device regenerate-key' + # Test swap keys 'az iot hub device renew-key' self.cmd( - '''iot hub device-identity regenerate-key -d {} -n {} -g {} --kt swap + '''iot hub device-identity renew-key -d {} -n {} -g {} --kt swap '''.format( edge_device_ids[1], LIVE_HUB, LIVE_RG ), @@ -541,8 +541,8 @@ def test_hub_devices(self): ], ) - # Test 'az iot hub device regenerate-key' with non sas authentication - self.cmd("iot hub device-identity regenerate-key -d {} -n {} -g {} --kt secondary" + # 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) From 580712fa94548a0b5271ba0ddc66ab834662059e Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 23 Nov 2020 16:22:01 -0800 Subject: [PATCH 154/179] Fix credscan OS. (#281) --- .azure-devops/merge.yml | 24 ++++++++++++++++++- CredScanSuppressions.json | 37 +++++++++++++++++++++++++++++ azext_iot/tests/test_iot_dps_int.py | 5 ++-- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 CredScanSuppressions.json diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 327681135..1dab1cdc0 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -123,8 +123,30 @@ jobs: - template: templates/install-configure-azure-cli.yml - job: CredScan - displayName: "Credential Scan" + displayName: 'Credential Scan' + pool: + vmImage: 'vs2017-win2016' + steps: - task: CredScan@3 inputs: + outputFormat: 'pre' scanFolder: '$(Build.SourcesDirectory)' + suppressionsFile: '$(Build.SourcesDirectory)/CredScanSuppressions.json' + + - task: PostAnalysis@1 + inputs: + AllTools: false + APIScan: false + BinSkim: false + CodesignValidation: false + CredScan: true + FortifySCA: false + FxCop: false + ModernCop: false + PoliCheck: false + RoslynAnalyzers: false + SDLNativeRules: false + Semmle: false + TSLint: false + ToolLogsNotFoundAction: 'Standard' diff --git a/CredScanSuppressions.json b/CredScanSuppressions.json new file mode 100644 index 000000000..3f349f26e --- /dev/null +++ b/CredScanSuppressions.json @@ -0,0 +1,37 @@ +{ + "tool": "Credential Scanner", + "suppressions": [ + { + "file": "azext_iot\\operations\\_mqtt.py", + "placeholder": "password=sas", + "_justification": "Client device simulation requires a SAS key extracted from IoT Hub to be passed in MQTT lib." + }, + { + "file": "azext_iot\\tests\\iothub\\test_iothub_discovery_unit.py", + "placeholder": "SharedAccessKey=AB+c/+5nm2XpDXcffhnGhnxz/TVF4m5ag7AuVIGwchj=", + "_justification": "Completely made up IoT Hub policy key for unit test." + }, + { + "file": "azext_iot\\tests\\iothub\\configurations\\test_edge_deployment.json", + "_justification": "Completely made up deployment with fake container registry creds." + }, + { + "file": "azext_iot\\tests\\conftest.py", + "_justification": "Completely made up keys for unit tests." + }, + { + "file": "azext_iot\\tests\\test_iot_dps_int.py", + "placeholder": "cT/EXZvsplPEpT//p98Pc6sKh8mY3kYgSxavHwMkl7w=", + "_justification": "Ensure made up endorsement key evaluates to the expected device key." + }, + { + "file": "azext_iot\\tests\\test_iot_dps_unit.py", + "_justification": "Completely made up keys for unit tests." + }, + { + "file": "azext_iot\\tests\\test_iot_ext_unit.py", + "placeholder": "+XLy+MVZ+aTeOnVzN2kLeB16O+kSxmz6g3rS6fAf6rw=", + "_justification": "Ensure made up policy key generates the proper SAS token." + } + ] +} \ No newline at end of file diff --git a/azext_iot/tests/test_iot_dps_int.py b/azext_iot/tests/test_iot_dps_int.py index 41cca7d11..ba78fa09d 100644 --- a/azext_iot/tests/test_iot_dps_int.py +++ b/azext_iot/tests/test_iot_dps_int.py @@ -9,6 +9,7 @@ from azext_iot.common.shared import EntityStatusType, AttestationType, AllocationType from azext_iot.common.certops import create_self_signed_certificate from azext_iot.common import embedded_cli +from azext_iot.common.utility import generate_key from azext_iot.iothub.providers.discovery import IotHubDiscovery from .settings import Setting @@ -300,8 +301,8 @@ def test_dps_enrollment_symmetrickey_lifecycle(self): enrollment_id = self.create_random_name("enrollment-for-test", length=48) enrollment_id2 = self.create_random_name("enrollment-for-test", length=48) attestation_type = AttestationType.symmetricKey.value - primary_key = "x3XNu1HeSw93rmtDXduRUZjhqdGbcqR/zloWYiyPUzw=" - secondary_key = "PahMnOSBblv9CRn5B765iK35jTvnjDUjYP9hKBZa4Ug=" + primary_key = generate_key() + secondary_key = generate_key() device_id = self.create_random_name("device-id-for-test", length=48) reprovisionPolicy_reprovisionandresetdata = "reprovisionandresetdata" hub_host_name = "{}.azure-devices.net".format(hub) From 232146ce160cb93af817b778e9b7262498986eca Mon Sep 17 00:00:00 2001 From: Jason Martinez Date: Thu, 3 Dec 2020 15:33:32 -0700 Subject: [PATCH 155/179] Remove link syntax. (#282) --- README.md | 2 +- docs/install-help.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3be36b881..8378708a6 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The resolution is to remove the deprecated `azure-cli-iot-ext` and install any v ## Commands -Please refer to the official `az iot` reference on [Microsoft Docs](https://docs.microsoft.com/en-us/cli/azure/ext/azure-iot/iot?view=azure-cli-latest) for a complete list of supported commands. You can also find IoT CLI usage tips on the [wiki](https://github.com/Azure/azure-iot-cli-extension/wiki/Tips). +Please refer to the official `az iot` reference on [Microsoft Docs](https://docs.microsoft.com/en-us/cli/azure/ext/azure-iot/iot) for a complete list of supported commands. You can also find IoT CLI usage tips on the [wiki](https://github.com/Azure/azure-iot-cli-extension/wiki/Tips). ## Installation diff --git a/docs/install-help.md b/docs/install-help.md index 4cbf9a1be..5813f4a73 100644 --- a/docs/install-help.md +++ b/docs/install-help.md @@ -23,7 +23,7 @@ After installing Azure CLI in my supported Linux environment, I try to install t Make sure you install the right distribution of Azure CLI that is compatible with your platform. -For example using the recommended installation path of [Linux via apt](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest), validate that your `/etc/apt/sources.list.d/azure-cli.list` file has the proper distribution identifier. +For example using the recommended installation path of [Linux via apt](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt), validate that your `/etc/apt/sources.list.d/azure-cli.list` file has the proper distribution identifier. On an Ubuntu 16.04 environment provided with the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install-win10) the sources list file should have an entry tagged with 'xenial': From 728e333d087d4dee9ae411514daf9024abf6c61c Mon Sep 17 00:00:00 2001 From: Ryan K Date: Fri, 4 Dec 2020 12:41:04 -0800 Subject: [PATCH 156/179] Identity storage test updates (#285) * Identity storage test updates * Tests will now enable identity and assign RBAC roles to test identity-based device-identity export * Address PR feedback --- CONTRIBUTING.md | 4 +- azext_iot/tests/test_iot_ext_int.py | 64 +++++++++++++++++++++++++++-- pytest.ini.example | 2 +- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75dccddae..1e2322253 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -130,12 +130,12 @@ You can either manually set the environment variables or use the `pytest.ini.exa azext_iot_testhub_cs="IoT Hub Connection String" azext_iot_testdps="IoT Hub DPS Name" azext_iot_teststorageuri="Blob Container SAS Uri" - azext_iot_testidentity=True + azext_iot_identity_teststorageid="Storage Account ID" ``` `azext_iot_teststorageuri` is optional and only required when you want to test device export and file upload functionality. You can generate a SAS Uri for your Blob container using the [Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/). You must also configure your IoT Hub's File Upload storage container via the Azure Portal for this test to pass. -`azext_iot_testidentity` is optional and only required when you want to test Identity-Based device export and file upload functionality. You must ensure that your IoT Hub has been created with a [Managed Service Identity](https://docs.microsoft.com/en-us/azure/iot-hub/virtual-network-support#create-an-iot-hub-with-managed-service-identity). Also, you must ensure that your hub has been granted the `Storage Blob Data Contributor` role on your storage account, and it is configured as your IoT Hub's File Upload storage container as described above. +`azext_iot_identity_teststorageid` is optional and only required when you want to test Identity-Based device export and file upload functionality. During this test, your hub will be assigned a System-Assigned AAD identity, and will be granted the role of "Storage Blob Data Contributor" on the storage account you provide. Both the hub's identity and the RBAC role will be removed once the test completes. ##### IoT Hub diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index b8b7cde90..63bd73fd7 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -13,7 +13,7 @@ from .settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC from azext_iot.constants import DEVICE_DEVICESCOPE_PREFIX -opt_env_set = ["azext_iot_teststorageuri", "azext_iot_testidentity"] +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 @@ -27,8 +27,8 @@ LIVE_STORAGE = settings.env.azext_iot_teststorageuri # Set this environment variable to enable identity-based integration tests -# You will need to have configured your IoT Hub and Storage Account before running. -LIVE_IDENTITY = settings.env.azext_iot_testidentity +# 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"] @@ -1437,9 +1437,51 @@ def test_storage(self): ) @pytest.mark.skipif( - not LIVE_IDENTITY, reason="azext_iot_testidentity env var not set" + not all([LIVE_STORAGE_ID, LIVE_STORAGE]), + reason="azext_iot_identity_teststorageid and azext_iot_teststorageuri env vars not set", ) def test_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"] + + if hub_identity.get("type", None) != identity_type_enable: + # enable hub identity and get ID + hub_identity = self.cmd( + 'iot hub update -n {} --set identity.type="{}"'.format( + LIVE_HUB, identity_type_enable + ) + ).get_output_in_json()["identity"] + + identity_enabled = True + + 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 + ) + ).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 + ) + ) + # give RBAC time to catch up + from time import sleep + sleep(30) + # identity-based device-identity export self.cmd( 'iot hub device-identity export -n {} --bcu "{}" --auth-type {}'.format( @@ -1453,6 +1495,20 @@ def test_identity_storage(self): ], ) + # 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 + ) + ) + self.cmd( + "iot hub update -n {} --set 'identity.type=\"{}\"'".format( + LIVE_HUB, identity_type_disable + ) + ) + class TestIoTEdgeOffline(IoTLiveScenarioTest): def __init__(self, test_case): diff --git a/pytest.ini.example b/pytest.ini.example index ac1def042..b89a4be91 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -17,7 +17,7 @@ env = azext_iot_testhub= azext_iot_testdps= azext_iot_teststorageuri= - azext_iot_testidentity= + azext_iot_teststorageid= azext_iot_central_app_id= azext_dt_ep_eventgrid_topic= azext_dt_ep_servicebus_namespace= From 2d2280a7c7a3e4866149ffd5c2bbfddbda73b692 Mon Sep 17 00:00:00 2001 From: Johnson Yang Date: Mon, 7 Dec 2020 11:46:44 -0800 Subject: [PATCH 157/179] Update az iot central device|device-template|api-token|diagnostic help string (#283) * Update az iot central device|device-template|api-token|diagnostic help strings * Fix Iot to IoT typo, add 2 lines before method for style check * fix style issues * remove tail space warning * remove extra whitespace * Share --device-id from az iot central, add allowed characters to az iot central device create * Fixed code review comments --- azext_iot/central/_help.py | 69 +++++++++++++++++++------------------ azext_iot/central/params.py | 66 ++++++++++++++++++++++++----------- 2 files changed, 82 insertions(+), 53 deletions(-) diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index f73335302..98cfc38de 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -55,7 +55,7 @@ def _load_central_devices_help(): "iot central device create" ] = """ type: command - short-summary: Create a device in IoT Central + short-summary: Create a device in IoT Central. examples: - name: Create a device @@ -77,7 +77,7 @@ def _load_central_devices_help(): "iot central device show" ] = """ type: command - short-summary: Get a device from IoT Central + short-summary: Get a device from IoT Central. examples: - name: Get a device @@ -91,7 +91,7 @@ def _load_central_devices_help(): "iot central device delete" ] = """ type: command - short-summary: Delete a device from IoT Central + short-summary: Delete a device from IoT Central. examples: - name: Delete a device @@ -105,7 +105,7 @@ def _load_central_devices_help(): "iot central device show-credentials" ] = """ type: command - short-summary: Get device credentials from IoT Central + short-summary: Get device credentials from IoT Central. examples: - name: Get device credentials for a device @@ -119,10 +119,9 @@ def _load_central_devices_help(): "iot central device registration-info" ] = """ type: command - short-summary: Get registration info on device(s) from IoT Central + short-summary: Get registration info on device(s) from IoT Central. long-summary: | - Note: This command can take a significant amount of time to return - if no device id is specified and your app contains a lot of devices + Note: This command can take a significant amount of time to return if no device id is specified and your app contains a lot of devices. examples: - name: Get registration info on specified device @@ -157,7 +156,10 @@ def _load_central_command_help(): "iot central device command history" ] = """ type: command - short-summary: Get most recent command-response request and response payload. + short-summary: Get the details for the latest command request and response sent to the device. + long-summary: | + Lists the most recent command request and response that was sent to the device from IoT Central. + Any update that the device performs to the device properties as a result of the command execution are not included in the response. examples: - name: Show command response text: > @@ -177,7 +179,7 @@ def _load_central_command_help(): Note: payload should be nested under "request". i.e. if your device expects the payload in a shape {"key": "value"} payload should be {"request": {"key": "value"}}. - --content can be pointed at a filepath too (.../path/to/payload.json) + --content can also be pointed at a filepath like this (.../path/to/payload.json) examples: - name: Run command response text: > @@ -276,15 +278,17 @@ def _load_central_api_token_help(): "iot central api-token" ] = """ type: group - short-summary: Create and Manage API tokens. + short-summary: Manage API tokens for your IoT Central application. + long-summary: IoT Central allows you to generate and manage API tokens to be used to access the IoT Central API. More information about APIs can be found at https://aka.ms/iotcentraldocsapi. """ helps[ "iot central api-token create" ] = """ type: command - short-summary: Create a new API token in the application - long-summary: The only time you will see the value of this token is when creating the token. Ensure you store this token somewhere securely, as if you lose it, you will need to create another. + short-summary: Generate an API token associated with your IoT Central application. + long-summary: | + Note: Write down your token once it's been generated as you won't be able to retrieve it again. examples: - name: Add new API token text: > @@ -292,14 +296,13 @@ def _load_central_api_token_help(): --token-id {tokenId} --app-id {appId} --role admin - """ helps[ "iot central api-token show" ] = """ type: command - short-summary: Get token meta data (e.g. role as a GUID, expiration) - long-summary: API token information contains basic information about the token and does not include the value of the token. + short-summary: Get details for an API token associated with your IoT Central application. + long-summary: List details, like its associated role, for an API token in your IoT Central app. examples: - name: Get API token text: > @@ -312,7 +315,7 @@ def _load_central_api_token_help(): "iot central api-token delete" ] = """ type: command - short-summary: Delete an API token from the application + short-summary: Delete an API token associated with your IoT Central application. examples: - name: Delete an API token text: > @@ -325,7 +328,7 @@ def _load_central_api_token_help(): "iot central api-token list" ] = """ type: command - short-summary: Get a list of all token meta data (e.g. Role as a GUID and expiration) + short-summary: List all API tokens associated with your IoT Central application. long-summary: Information in the list contains basic information about the tokens in the application and does not include token values. examples: - name: List of API tokens @@ -348,7 +351,7 @@ def _load_central_device_templates_help(): "iot central device-template create" ] = """ type: command - short-summary: Create a device template in IoT Central + short-summary: Create a device template in IoT Central. examples: - name: Create a device template with payload read from a file @@ -370,7 +373,7 @@ def _load_central_device_templates_help(): "iot central device-template show" ] = """ type: command - short-summary: Get a device template from IoT Central + short-summary: Get a device template from IoT Central. examples: - name: Get a device template @@ -384,9 +387,9 @@ def _load_central_device_templates_help(): "iot central device-template delete" ] = """ type: command - short-summary: Delete a device template from IoT Central + short-summary: Delete a device template from IoT Central. long-summary: | - Note: this is expected to fail if any devices are still associated to this template. + Note: this is expected to fail if any devices are still associated to this template. examples: - name: Delete a device template from IoT Central @@ -410,11 +413,12 @@ def _load_central_monitors_help(): "iot central diagnostics monitor-events" ] = """ type: command - short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. + short-summary: View device telemetry messages sent to the IoT Central app. long-summary: | - EXPERIMENTAL requires Python 3.5+ - This command relies on and may install dependent Cython package (uamqp) upon first execution. - https://github.com/Azure/azure-uamqp-python + Shows the telemetry data sent to IoT Central application. By default, + it shows all the data sent by all devices. Use the --device-id parameter + to filter to a specific device. + examples: - name: Basic usage text: > @@ -454,9 +458,7 @@ def _load_central_monitors_help(): type: command short-summary: Validate messages sent to the IoT Hub for an IoT Central app. long-summary: | - EXPERIMENTAL requires Python 3.5+ - This command relies on and may install dependent Cython package (uamqp) upon first execution. - https://github.com/Azure/azure-uamqp-python + Performs validations on the telemetry messages and reports back data that is not modeled in the device template or data where the data type doesn’t match what is defined in the device template. examples: - name: Basic usage text: > @@ -485,7 +487,7 @@ def _load_central_monitors_help(): "iot central diagnostics monitor-properties" ] = """ type: command - short-summary: Monitor desired and reported properties sent to/from the IoT Hub for an IoT Central app. + short-summary: View desired and reported properties sent to/from the IoT Central app. long-summary: | Polls device-twin from central and compares it to the last device-twin Parses out properties from device-twin, and detects if changes were made @@ -500,12 +502,12 @@ def _load_central_monitors_help(): "iot central diagnostics validate-properties" ] = """ type: command - short-summary: Validate reported properties sent to IoT Central app. + short-summary: Validate reported properties sent to the IoT Central application. long-summary: | Performs validations on reported property updates: 1) Warning - Properties sent by device that are not modeled in central. 2) Warning - Properties with same name declared in multiple interfaces - should have interface name included as part of the property update. + should have interface name included as part of the property update. examples: - name: Basic usage text: > @@ -516,7 +518,7 @@ def _load_central_monitors_help(): "iot central diagnostics registration-summary" ] = """ type: command - short-summary: Provides a registration summary of all the devices in an app. + short-summary: View the registration summary of all the devices in an app. long-summary: | Note: This command can take a significant amount of time to return if your app contains a lot of devices @@ -554,7 +556,8 @@ def _load_central_deprecated_commands(): "iot central device twin show" ] = """ type: command - short-summary: Get the device twin from IoT Hub. + short-summary: Get the device twin from IoT Central application. + long-summary: Returns back the desired and reported device properties from the IoT Central application. """ helps[ diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 02cceb5a8..98361d705 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -23,7 +23,9 @@ role_type = CLIArgumentType( options_list=["--role", "-r"], choices=CaseInsensitiveList([role.name for role in Role]), - help="Role for the user/service-principal you are adding to the app.", + help="The role that will be associated with this token." + " You can specify one of the built-in roles, or specify the role ID of a custom role." + " See more at https://aka.ms/iotcentral-customrolesdocs", ) style_type = CLIArgumentType( @@ -39,42 +41,58 @@ def load_central_arguments(self, _): Load CLI Args for Knack parser """ with self.argument_context("iot central") as context: - context.argument("app_id", options_list=["--app-id", "-n"], help="Target App.") + context.argument( + "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." + ) context.argument( "token", options_list=["--token"], - help="Authorization token for request. " - "More info available here: https://docs.microsoft.com/en-us/learn/modules/manage-iot-central-apps-with-rest-api/ " - "MUST INCLUDE type (e.g. 'SharedAccessToken ...', 'Bearer ...'). " - "Example: 'Bearer someBearerTokenHere'", + help="If you'd prefer to submit your request without authenticating against the Azure CLI, you can specify a valid" + " user token to authenticate your request. You must specify the type of key as part of the request." + " Learn more at https://aka.ms/iotcentraldocsapi", ) context.argument( "central_dns_suffix", options_list=["--central-dns-suffix", "--central-api-uri"], - help="Central dns suffix. " - "This enables running cli commands against non public/prod environments", + help="The IoT Central DNS suffix associated with your application. Default value is: azureiotcentral.com", + ) + context.argument( + "device_id", + options_list=["--device-id", "-d"], + help="The ID of the target device, " + "You can find the Device Id by clicking on the Connect button on the Device Details page.", ) with self.argument_context("iot central device-template") as context: context.argument( "device_template_id", options_list=["--device-template-id", "--dtid"], - help="Device template id. Example: somedevicetemplate", + help="The ID of the target device template. Example: somedevicetemplate", ) context.argument( "content", options_list=["--content", "-k"], - help="Configuration for request. " - "Provide path to JSON file or raw stringified JSON. " - "[File Path Example: ./path/to/file.json] " - "[Stringified JSON Example: {'a': 'b'}] ", + help="The device template definition. Provide path to JSON file or raw stringified JSON." + " [File Path Example: ./path/to/file.json] [Example of stringified JSON: {}]." + " The request body must contain CapabilityModel.", + ) + + with self.argument_context("iot central device-template create") as context: + context.argument( + "device_template_id", + options_list=["--device-template-id", "--dtid"], + help="Unique ID for the Device template.", ) with self.argument_context("iot central api-token") as context: context.argument( "token_id", options_list=["--token-id", "--tkid"], - help="Unique ID for the API token. ", + help="The IoT Central ID associated with this token, [0-9a-zA-Z\\-] allowed, max length limit to 40." + " Specify an ID that you'll then use when modifying or deleting this token later via the CLI or API.", ) context.argument("role", arg_type=role_type) @@ -106,12 +124,14 @@ def load_central_arguments(self, _): context.argument( "interface_id", options_list=["--interface-id", "-i"], - help="Interface name as specified in the device template. Example: c2dTestingTemplate_356", + help="The name of the interface as specified in the device template. You can find it by navigating to Device" + " Template and view the interface identity under the corresponding device capability.", ) context.argument( "command_name", options_list=["--command-name", "--cn"], - help="Command name as specified in device template. Example: run_firmware_update", + help="The command name as specified in the device template. Command name could be different from the Display" + " Name of the command.", ) context.argument( "content", @@ -122,6 +142,15 @@ def load_central_arguments(self, _): "[Stringified JSON Example: {'a': 'b'}] ", ) + with self.argument_context("iot central device create") as context: + context.argument( + "device_id", + options_list=["--device-id", "-d"], + help="Provide a unique identifier for the device." + " A case-sensitive string (up to 128 characters long) of ASCII 7-bit alphanumeric characters plus" + " certain special characters: - . + % _ # * ? ! ( ) , : = @ $ '", + ) + with self.argument_context("iot central user") as context: context.argument( "tenant_id", @@ -148,9 +177,6 @@ def load_central_arguments(self, _): with self.argument_context("iot central diagnostics") as context: context.argument("timeout", arg_type=event_timeout_type) context.argument("properties", arg_type=event_msg_prop_type) - context.argument( - "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", - ) context.argument("minimum_severity", arg_type=severity_type) context.argument("style", arg_type=style_type) context.argument( @@ -168,7 +194,7 @@ def load_central_arguments(self, _): "Use 0 for infinity.", ) context.argument( - "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", + "module_id", options_list=["--module-id", "-m"], help="Provide IoT Edge Module ID if the device type is IoT Edge.", ) # TODO: Delete this by end of Dec 2020 load_deprecated_params(self, _) From 46fbc890a8b5e0c1dc6d6ab11748614f6769d980 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 7 Dec 2020 13:22:33 -0800 Subject: [PATCH 158/179] Support explicit etag across IoT Hub resources. (#286) * Support explicit etag across IoT Hub resources. * Temp comment out IT caused by inconsistent hub policy. --- HISTORY.rst | 22 + azext_iot/_params.py | 15 +- .../common/digitaltwin_sas_token_auth.py | 65 --- azext_iot/common/utility.py | 16 - azext_iot/constants.py | 3 +- azext_iot/operations/hub.py | 167 ++---- .../configurations/test_iot_config_unit.py | 51 +- azext_iot/tests/test_iot_ext_int.py | 7 +- azext_iot/tests/test_iot_ext_unit.py | 515 ++++++++++-------- 9 files changed, 387 insertions(+), 474 deletions(-) delete mode 100644 azext_iot/common/digitaltwin_sas_token_auth.py diff --git a/HISTORY.rst b/HISTORY.rst index 0d24273c0..b15e2656a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,28 @@ Release History =============== +0.10.8 ++++++++++++++++ + +**IoT Hub updates** + +The following commands support an explicit etag parameter. If no etag arg is passed the value "*" is used. + +* az iot hub device-identity update +* az iot hub device-identity delete +* az iot hub device-identity renew-key +* az iot hub device-twin update +* az iot hub device-twin delete +* az iot hub module-identity update +* az iot hub module-identity delete +* az iot hub module-twin update +* az iot hub module-twin delete +* az iot hub configuration update +* az iot hub configuration delete +* az iot edge deployment update +* az iot edge deployment update + + 0.10.7 +++++++++++++++ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 58839506e..8d0520fb4 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -114,7 +114,10 @@ def load_arguments(self, _): help="Valid token duration in seconds.", ) context.argument( - "etag", options_list=["--etag", "-e"], help="Entity tag value." + "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." ) context.argument( "top", @@ -180,16 +183,6 @@ def load_arguments(self, _): arg_type=get_three_state_flag(), help="Reinstall uamqp dependency compatible with extension version. Default: false", ) - context.argument( - "repo_endpoint", - options_list=["--endpoint", "-e"], - help="IoT Plug and Play endpoint.", - ) - context.argument( - "repo_id", - options_list=["--repo-id", "-r"], - help="IoT Plug and Play repository Id.", - ) context.argument( "consumer_group", options_list=["--consumer-group", "--cg", "-c"], diff --git a/azext_iot/common/digitaltwin_sas_token_auth.py b/azext_iot/common/digitaltwin_sas_token_auth.py deleted file mode 100644 index c2abbe108..000000000 --- a/azext_iot/common/digitaltwin_sas_token_auth.py +++ /dev/null @@ -1,65 +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. -# -------------------------------------------------------------------------------------------- -""" -digitaltwin_sas_token_auth: Module containing DigitalTwin Model Shared Access Signature token class. - -""" - -from base64 import b64encode, b64decode -from hashlib import sha256 -from hmac import HMAC -from time import time -try: - from urllib import (urlencode, quote_plus) -except ImportError: - from urllib.parse import (urlencode, quote_plus) -from msrest.authentication import Authentication - - -class DigitalTwinSasTokenAuthentication(Authentication): - """ - Shared Access Signature authorization for DigitalTwin Repository. - - Args: - uri (str): Uri of target resource. - shared_access_policy_name (str): Name of shared access policy. - shared_access_key (str): Shared access key. - expiry (int): Expiry of the token to be generated. Input should - be seconds since the epoch, in UTC. Default is an hour later from now. - """ - def __init__(self, repositoryId, endpoint, shared_access_key_name, shared_access_key, expiry=None): - self.repositoryId = repositoryId - self.policy = shared_access_key_name - self.key = shared_access_key - self.endpoint = endpoint - if expiry is None: - self.expiry = time() + 3600 # Default expiry is an hour later - else: - self.expiry = expiry - - def generate_sas_token(self): - """ - Create a shared access signiture token as a string literal. - - Returns: - result (str): SAS token as string literal. - """ - encoded_uri = quote_plus(self.endpoint) - encoded_repo_id = quote_plus(self.repositoryId) - ttl = int(self.expiry) - sign_key = '%s\n%s\n%d' % (encoded_repo_id, encoded_uri, ttl) - signature = b64encode(HMAC(b64decode(self.key), sign_key.encode('utf-8'), sha256).digest()) - result = { - 'sr': self.endpoint, - 'sig': signature, - 'se': str(ttl) - } - - if self.policy: - result['skn'] = self.policy - result['rid'] = self.repositoryId - - return 'SharedAccessSignature ' + urlencode(result) diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index 1e21672fe..ff2f2a128 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -380,22 +380,6 @@ def init_monitoring(cmd, timeout, properties, enqueued_time, repair, yes): return (enqueued_time, properties, timeout, output) -def get_sas_token(target): - from azext_iot.common.digitaltwin_sas_token_auth import ( - DigitalTwinSasTokenAuthentication, - ) - - token = "" - if target.get("repository_id"): - token = DigitalTwinSasTokenAuthentication( - target["repository_id"], - target["entity"], - target["policy"], - target["primarykey"], - ).generate_sas_token() - return {"Authorization": "{}".format(token)} - - def dict_clean(d): """ Remove None from dictionary """ if not isinstance(d, dict): diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 239b7b3e0..5d80ead0e 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,14 +7,13 @@ import os -VERSION = "0.10.7" +VERSION = "0.10.8" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" EDGE_DEPLOYMENT_SCHEMA_2_PATH = os.path.join( EXTENSION_ROOT, "assets", "edge-deploy-2.0.schema.json" ) -BASE_API_VERSION = "2018-08-30-preview" BASE_MQTT_API_VERSION = "2018-06-30" MESSAGING_HTTP_C2D_SYSTEM_PROPERTIES = [ "iothub-messageid", diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 8e70e5bc9..bceb2f9f3 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -327,7 +327,7 @@ def update_iot_device_custom( def iot_device_update( - cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None + cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -346,7 +346,7 @@ def iot_device_update( parameters.get('statusReason'), parameters.get('deviceScope') ) - updated_device.etag = parameters.get("etag", "*") + updated_device.etag = etag if etag else "*" return _iot_device_update(target, device_id, updated_device) @@ -367,7 +367,7 @@ def _iot_device_update(target, device_id, device): def iot_device_delete( - cmd, device_id, hub_name=None, resource_group_name=None, login=None + cmd, device_id, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -377,47 +377,35 @@ def iot_device_delete( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - device = _iot_device_show(target=target, device_id=device_id) - etag = device.get("etag") - - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - service_sdk.devices.delete_identity( - id=device_id, custom_headers=headers - ) - return - raise LookupError("device etag not found") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + service_sdk.devices.delete_identity( + id=device_id, custom_headers=headers + ) + return except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) -def _update_device_key(target, device, auth_method, pk, sk): +def _update_device_key(target, device, auth_method, pk, sk, etag=None): resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) try: auth = _assemble_auth(auth_method, pk, sk) device["authentication"] = auth - etag = device.get("etag", None) - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.devices.create_or_update_identity( - id=device["deviceId"], - device=device, - custom_headers=headers, - ) - raise LookupError("device etag not found.") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + return service_sdk.devices.create_or_update_identity( + id=device["deviceId"], + device=device, + custom_headers=headers, + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) -def iot_device_key_regenerate(cmd, hub_name, device_id, renew_key_type, resource_group_name=None, login=None): +def iot_device_key_regenerate(cmd, hub_name, device_id, renew_key_type, resource_group_name=None, login=None, etag=None): discovery = IotHubDiscovery(cmd) target = discovery.get_target( hub_name=hub_name, resource_group_name=resource_group_name, login=login @@ -438,7 +426,7 @@ 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) + return _update_device_key(target, device, device["authentication"]["type"], pk, sk, etag) def iot_device_get_parent( @@ -731,6 +719,7 @@ def iot_device_module_update( hub_name=None, resource_group_name=None, login=None, + etag=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -741,21 +730,16 @@ def iot_device_module_update( try: updated_module = _handle_module_update_params(parameters) - etag = parameters.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.modules.create_or_update_identity( - id=device_id, - mid=module_id, - module=updated_module, - custom_headers=headers, - ) - raise LookupError("module etag not found.") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + return service_sdk.modules.create_or_update_identity( + id=device_id, + mid=module_id, + module=updated_module, + custom_headers=headers, + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def _handle_module_update_params(parameters): @@ -829,7 +813,7 @@ 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 + cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -839,22 +823,14 @@ def iot_device_module_delete( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - module = _iot_device_module_show( - target=target, device_id=device_id, module_id=module_id + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + service_sdk.modules.delete_identity( + id=device_id, mid=module_id, custom_headers=headers ) - etag = module.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - service_sdk.modules.delete_identity( - id=device_id, mid=module_id, custom_headers=headers - ) - return - raise LookupError("module etag not found") + return except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def iot_device_module_twin_show( @@ -889,6 +865,7 @@ def iot_device_module_twin_update( hub_name=None, resource_group_name=None, login=None, + etag=None ): from azext_iot.common.utility import verify_transform @@ -901,7 +878,7 @@ def iot_device_module_twin_update( try: headers = {} - headers["If-Match"] = '"*"' + headers["If-Match"] = '"{}"'.format(etag if etag else "*") verify = {} if parameters.get("properties"): if parameters["properties"].get("desired"): @@ -929,6 +906,7 @@ def iot_device_module_twin_replace( hub_name=None, resource_group_name=None, login=None, + etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -939,24 +917,16 @@ def iot_device_module_twin_replace( try: target_json = process_json_arg(target_json, argument_name="json") - module = _iot_device_module_twin_show( - target=target, device_id=device_id, module_id=module_id + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + return service_sdk.modules.replace_twin( + id=device_id, + mid=module_id, + device_twin_info=target_json, + custom_headers=headers, ) - etag = module.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.modules.replace_twin( - id=device_id, - mid=module_id, - device_twin_info=target_json, - custom_headers=headers, - ) - raise LookupError("module twin etag not found") except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def iot_edge_set_modules( @@ -1204,7 +1174,7 @@ def _validate_payload_schema(content): def iot_hub_configuration_update( - cmd, config_id, parameters, hub_name=None, resource_group_name=None, login=None + cmd, config_id, parameters, hub_name=None, resource_group_name=None, login=None, etag=None ): from azext_iot.sdk.iothub.service.models import Configuration from azext_iot.common.utility import verify_transform @@ -1217,11 +1187,8 @@ def iot_hub_configuration_update( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - etag = parameters.get("etag") - if not etag: - raise LookupError("invalid request, configuration etag not found") headers = {} - headers["If-Match"] = '"{}"'.format(etag) + headers["If-Match"] = '"{}"'.format(etag if etag else "*") verify = {"metrics": dict, "metrics.queries": dict, "content": dict} if parameters.get("labels"): verify["labels"] = dict @@ -1233,15 +1200,14 @@ def iot_hub_configuration_update( content=parameters["content"], metrics=parameters.get("metrics", None), target_condition=parameters["targetCondition"], - priority=parameters["priority"], - content_type="assignment", + priority=parameters["priority"] ) return service_sdk.configuration.create_or_update( id=config_id, configuration=config, custom_headers=headers ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except (AttributeError, LookupError, TypeError) as err: + except (AttributeError, TypeError) as err: raise CLIError(err) @@ -1319,7 +1285,7 @@ def _iot_hub_configuration_list( def iot_hub_configuration_delete( - cmd, config_id, hub_name=None, resource_group_name=None, login=None + cmd, config_id, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -1329,18 +1295,11 @@ def iot_hub_configuration_delete( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - config = _iot_hub_configuration_show(target=target, config_id=config_id) - etag = config.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - service_sdk.configuration.delete(id=config_id, custom_headers=headers) - return - raise LookupError("configuration etag not found") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + service_sdk.configuration.delete(id=config_id, custom_headers=headers) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def iot_edge_deployment_metric_show( @@ -1450,7 +1409,7 @@ 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 + cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None, etag=None ): from azext_iot.common.utility import verify_transform @@ -1463,7 +1422,7 @@ def iot_device_twin_update( try: headers = {} - headers["If-Match"] = '"*"' + headers["If-Match"] = '"{}"'.format(etag if etag else "*") verify = {} if parameters.get("properties"): if parameters["properties"].get("desired"): @@ -1481,7 +1440,7 @@ def iot_device_twin_update( def iot_device_twin_replace( - cmd, device_id, target_json, hub_name=None, resource_group_name=None, login=None + cmd, device_id, target_json, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -1492,19 +1451,13 @@ def iot_device_twin_replace( try: target_json = process_json_arg(target_json, argument_name="json") - device = _iot_device_twin_show(target=target, device_id=device_id) - etag = device.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.devices.replace_twin( - id=device_id, device_twin_info=target_json, custom_headers=headers - ) - raise LookupError("device twin etag not found") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + return service_sdk.devices.replace_twin( + id=device_id, device_twin_info=target_json, custom_headers=headers + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def iot_device_method( diff --git a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py index 5b185427f..1edcac871 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py @@ -5,6 +5,7 @@ # -------------------------------------------------------------------------------------------- +from azext_iot.tests.generators import generate_generic_id import pytest import responses import json @@ -563,21 +564,21 @@ def test_config_create_error( class TestConfigDelete: - @pytest.fixture(params=[(200, 204)]) + @pytest.fixture(params=[204]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - etag = str(uuid4()) - service_client.expected_etag = etag side_effect = [ - build_mock_response(mocker, request.param[0], {"etag": etag}), - build_mock_response(mocker, request.param[1]), + build_mock_response(mocker, request.param), ] service_client.side_effect = side_effect return service_client - def test_config_delete(self, serviceclient, fixture_cmd): + @pytest.mark.parametrize( + "etag", [generate_generic_id(), None], + ) + def test_config_delete(self, serviceclient, fixture_cmd, etag): subject.iot_hub_configuration_delete( - fixture_cmd, config_id=config_id, hub_name=mock_target["entity"] + fixture_cmd, config_id=config_id, hub_name=mock_target["entity"], etag=etag ) args = serviceclient.call_args url = args[0][0].url @@ -586,19 +587,7 @@ def test_config_delete(self, serviceclient, fixture_cmd): assert method == "DELETE" assert "{}/configurations/{}?".format(mock_target["entity"], config_id) in url - assert headers["If-Match"] == '"{}"'.format(serviceclient.expected_etag) - - @pytest.mark.parametrize("expected_error", [CLIError]) - def test_config_delete_invalid_args( - self, - fixture_cmd, - serviceclient_generic_invalid_or_missing_etag, - expected_error, - ): - with pytest.raises(expected_error): - subject.iot_hub_configuration_delete( - fixture_cmd, config_id=config_id, hub_name=mock_target["entity"] - ) + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") def test_config_delete_error(self, fixture_cmd, serviceclient_generic_error): with pytest.raises(CLIError): @@ -614,12 +603,16 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client.return_value = build_mock_response(mocker, request.param, {}) return service_client - def test_config_update(self, fixture_cmd, serviceclient, sample_config_show): + @pytest.mark.parametrize( + "etag", [generate_generic_id(), None], + ) + def test_config_update(self, fixture_cmd, serviceclient, sample_config_show, etag): subject.iot_hub_configuration_update( cmd=fixture_cmd, config_id=config_id, hub_name=mock_target["entity"], parameters=sample_config_show, + etag=etag ) args = serviceclient.call_args url = args[0][0].url @@ -630,30 +623,20 @@ def test_config_update(self, fixture_cmd, serviceclient, sample_config_show): assert "{}/configurations/{}?".format(mock_target["entity"], config_id) in url assert method == "PUT" - assert headers["If-Match"] == '"{}"'.format(sample_config_show["etag"]) - assert body["id"] == sample_config_show["id"] assert body.get("metrics") == sample_config_show.get("metrics") assert body.get("targetCondition") == sample_config_show.get("targetCondition") assert body.get("priority") == sample_config_show.get("priority") assert body.get("labels") == sample_config_show.get("labels") + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") + def test_config_update_invalid_args( self, fixture_cmd, serviceclient, sample_config_show ): from copy import deepcopy - request = deepcopy(sample_config_show) - request["etag"] = None - - with pytest.raises(CLIError): - subject.iot_hub_configuration_update( - cmd=fixture_cmd, - config_id=config_id, - hub_name=mock_target["entity"], - parameters=request, - ) - request = deepcopy(sample_config_show) request["labels"] = "not a dictionary" diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 63bd73fd7..925ab99d0 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -76,9 +76,10 @@ def test_hub(self): LIVE_HUB) conn_str_eventhub_pattern = r'^Endpoint=sb://' - 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) + # TODO: Temporarily disable to support warning on missing policy. + # 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) diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index 02555b21d..4e79a2234 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -4,19 +4,18 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -''' +""" NOTICE: These tests are to be phased out and introduced in more modern form. Try not to add any new content, only fixes if necessary. Look at IoT Hub jobs or configuration tests for a better example. Also use responses fixtures like mocked_response for http request mocking. -''' +""" import pytest import json import os import responses import re -from uuid import uuid4 from azext_iot.operations import hub as subject from azext_iot.common.utility import ( validate_min_python_version, @@ -43,11 +42,7 @@ module_id = "mymod" config_id = "myconfig" message_etag = "3k28zb44-0d00-4ddd-ade3-6110eb94c476" -c2d_purge_response = { - "deviceId": device_id, - "moduleId": None, - "totalMessagesPurged": 3 -} +c2d_purge_response = {"deviceId": device_id, "moduleId": None, "totalMessagesPurged": 3} generic_cs_template = "HostName={};SharedAccessKeyName={};SharedAccessKey={}" @@ -277,10 +272,9 @@ def test_device_create_addchildren(self, sc_device_create_addchildren, req): 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")] - ) + 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( @@ -322,7 +316,10 @@ def test_device_create_addchildren_invalid_args( @pytest.mark.parametrize( "req, exp", [ - (generate_device_create_req(ee=True, auth="x509_thumbprint", ptp=None), ValueError), + ( + generate_device_create_req(ee=True, auth="x509_thumbprint", ptp=None), + ValueError, + ), (generate_device_create_req(auth="doesnotexist"), ValueError), ( generate_device_create_req(auth="x509_thumbprint", ptp=None, stp=""), @@ -363,9 +360,8 @@ def generate_device_show(**kvp): "authentication": { "symmetricKey": {"primaryKey": None, "secondaryKey": None}, "x509Thumbprint": {"primaryThumbprint": None, "secondaryThumbprint": None}, - "type": "sas" + "type": "sas", }, - "etag": "abcd", "capabilities": {"iotEdge": True}, "deviceId": device_id, "status": "disabled", @@ -385,7 +381,7 @@ def device_update_con_arg( primary_thumbprint=None, secondary_thumbprint=None, primary_key=None, - secondary_key=None + secondary_key=None, ): return { "edge_enabled": edge_enabled, @@ -395,7 +391,7 @@ def device_update_con_arg( "primary_thumbprint": primary_thumbprint, "secondary_thumbprint": secondary_thumbprint, "primary_key": primary_key, - "secondary_key": secondary_key + "secondary_key": secondary_key, } @@ -431,12 +427,21 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): } ) ), - (generate_device_show(authentication={"type": "certificateAuthority"})), + ( + generate_device_show( + authentication={"type": "certificateAuthority"}, + etag=generate_generic_id(), + ) + ), ], ) def test_device_update(self, fixture_cmd, serviceclient, req): subject.iot_device_update( - fixture_cmd, req["deviceId"], hub_name=mock_target["entity"], parameters=req + fixture_cmd, + req["deviceId"], + hub_name=mock_target["entity"], + parameters=req, + etag=req.get("etag"), ) args = serviceclient.call_args assert ( @@ -457,76 +462,65 @@ def test_device_update(self, fixture_cmd, serviceclient, req): assert body["authentication"]["x509Thumbprint"]["secondaryThumbprint"] headers = args[0][0].headers - assert headers["If-Match"] == '"{}"'.format(req["etag"]) + target_etag = req.get("etag") + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") @pytest.mark.parametrize( "req, arg", [ ( - generate_device_show( - capabilities={"iotEdge": False} - ), - device_update_con_arg( - edge_enabled=True - ) + generate_device_show(capabilities={"iotEdge": False}), + device_update_con_arg(edge_enabled=True), ), ( - generate_device_show( - status="disabled" - ), - device_update_con_arg( - status="enabled" - ) - ), - ( - generate_device_show(), - device_update_con_arg( - status_reason="test" - ) + generate_device_show(status="disabled"), + device_update_con_arg(status="enabled"), ), + (generate_device_show(), device_update_con_arg(status_reason="test")), ( generate_device_show(), device_update_con_arg( auth_method="shared_private_key", primary_key="primarykeyUpdated", - secondary_key="secondarykeyUpdated" - ) + secondary_key="secondarykeyUpdated", + ), ), ( generate_device_show( authentication={ "type": "selfSigned", "symmetricKey": {"primaryKey": None, "secondaryKey": None}, - "x509Thumbprint": {"primaryThumbprint": "123", "secondaryThumbprint": "321"}, + "x509Thumbprint": { + "primaryThumbprint": "123", + "secondaryThumbprint": "321", + }, } ), device_update_con_arg( auth_method="shared_private_key", primary_key="primary_key", - secondary_key="secondary_key" - ) + secondary_key="secondary_key", + ), ), ( generate_device_show( authentication={ "type": "certificateAuthority", "symmetricKey": {"primaryKey": None, "secondaryKey": None}, - "x509Thumbprint": {"primaryThumbprint": None, "secondaryThumbprint": None}, + "x509Thumbprint": { + "primaryThumbprint": None, + "secondaryThumbprint": None, + }, } ), device_update_con_arg( auth_method="x509_thumbprint", primary_thumbprint="primary_thumbprint", - secondary_thumbprint="secondary_thumbprint" - ) - ), - ( - generate_device_show(), - device_update_con_arg( - auth_method="x509_ca", - ) + secondary_thumbprint="secondary_thumbprint", + ), ), - ] + (generate_device_show(), device_update_con_arg(auth_method="x509_ca",)), + ], ) def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): instance = subject.update_iot_device_custom( @@ -534,11 +528,11 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): arg["edge_enabled"], arg["status"], arg["status_reason"], - arg['auth_method'], - arg['primary_thumbprint'], - arg['secondary_thumbprint'], - arg['primary_key'], - arg["secondary_key"] + arg["auth_method"], + arg["primary_thumbprint"], + arg["secondary_thumbprint"], + arg["primary_key"], + arg["secondary_key"], ) if arg["edge_enabled"]: @@ -546,20 +540,28 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): if arg["status"]: assert instance["status"] == arg["status"] if arg["status_reason"]: - assert instance['statusReason'] == arg["status_reason"] + assert instance["statusReason"] == arg["status_reason"] if arg["auth_method"]: if arg["auth_method"] == "shared_private_key": - assert instance['authentication']['type'] == "sas" - instance['authentication']['symmetricKey']['primaryKey'] == arg['primary_key'] - instance['authentication']['symmetricKey']['secondaryKey'] == arg['secondary_key'] + assert instance["authentication"]["type"] == "sas" + instance["authentication"]["symmetricKey"]["primaryKey"] == arg[ + "primary_key" + ] + instance["authentication"]["symmetricKey"]["secondaryKey"] == arg[ + "secondary_key" + ] if arg["auth_method"] == "x509_thumbprint": - assert instance['authentication']['type'] == "selfSigned" - if arg['primary_thumbprint']: - instance['authentication']['x509Thumbprint']['primaryThumbprint'] = arg['primary_thumbprint'] - if arg['secondary_thumbprint']: - instance['authentication']['x509Thumbprint']['secondaryThumbprint'] = arg['secondary_thumbprint'] - if arg['auth_method'] == "x509_ca": - assert instance['authentication']['type'] == "certificateAuthority" + assert instance["authentication"]["type"] == "selfSigned" + if arg["primary_thumbprint"]: + instance["authentication"]["x509Thumbprint"][ + "primaryThumbprint" + ] = arg["primary_thumbprint"] + if arg["secondary_thumbprint"]: + instance["authentication"]["x509Thumbprint"][ + "secondaryThumbprint" + ] = arg["secondary_thumbprint"] + if arg["auth_method"] == "x509_ca": + assert instance["authentication"]["type"] == "certificateAuthority" @pytest.mark.parametrize( "req, arg, exp", @@ -567,26 +569,21 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): ( generate_device_show(), device_update_con_arg( - auth_method="shared_private_key", - primary_key="primarykeyUpdated", + auth_method="shared_private_key", primary_key="primarykeyUpdated", ), - CLIError + CLIError, ), ( generate_device_show(), - device_update_con_arg( - auth_method="x509_thumbprint", - ), - CLIError + device_update_con_arg(auth_method="x509_thumbprint",), + CLIError, ), ( generate_device_show(), - device_update_con_arg( - auth_method="Unknown", - ), - ValueError + device_update_con_arg(auth_method="Unknown",), + ValueError, ), - ] + ], ) def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): with pytest.raises(exp): @@ -595,11 +592,11 @@ def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): arg["edge_enabled"], arg["status"], arg["status_reason"], - arg['auth_method'], - arg['primary_thumbprint'], - arg['secondary_thumbprint'], - arg['primary_key'], - arg["secondary_key"] + arg["auth_method"], + arg["primary_thumbprint"], + arg["secondary_thumbprint"], + arg["primary_key"], + arg["secondary_key"], ) @pytest.mark.parametrize( @@ -617,7 +614,7 @@ def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): ), CLIError, ), - (generate_device_show(authentication={"type": "doesnotexist"}), CLIError) + (generate_device_show(authentication={"type": "doesnotexist"}), CLIError), ], ) def test_device_update_invalid_args(self, serviceclient, req, exp): @@ -648,24 +645,24 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): kvp.setdefault( "authentication", { - "symmetricKey": { - "primaryKey": "123", - "secondaryKey": "321" - }, - "type": "sas" - } + "symmetricKey": {"primaryKey": "123", "secondaryKey": "321"}, + "type": "sas", + }, ) test_side_effect = [ build_mock_response(mocker, 200, generate_device_show(**kvp)), - build_mock_response(mocker, 200, {}) + build_mock_response(mocker, 200, {}), ] service_client.side_effect = test_side_effect return service_client - @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) - def test_device_key_regenerate(self, fixture_cmd, serviceclient, req): + @pytest.mark.parametrize( + "req, etag", + [("primary", generate_generic_id()), ("secondary", None), ("swap", None)], + ) + def test_device_key_regenerate(self, fixture_cmd, serviceclient, req, etag): subject.iot_device_key_regenerate( - fixture_cmd, mock_target["entity"], device_id, req + fixture_cmd, mock_target["entity"], device_id, req, etag=etag ) args = serviceclient.call_args assert ( @@ -674,14 +671,17 @@ def test_device_key_regenerate(self, fixture_cmd, serviceclient, req): assert args[0][0].method == "PUT" body = json.loads(args[0][0].body) - if(req == "primary"): + if req == "primary": assert body["authentication"]["symmetricKey"]["primaryKey"] != "123" - if(req == "secondary"): + if req == "secondary": assert body["authentication"]["symmetricKey"]["secondaryKey"] != "321" - if(req == "swap"): + if req == "swap": assert body["authentication"]["symmetricKey"]["primaryKey"] == "321" assert body["authentication"]["symmetricKey"]["secondaryKey"] == "123" + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") + @pytest.fixture(params=[200]) def serviceclient_invalid_args(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) @@ -694,14 +694,11 @@ def serviceclient_invalid_args(self, mocker, fixture_ghcs, fixture_sas, request) return service_client @pytest.mark.parametrize( - "req, exp", - [ - ("primary", CLIError), - ("secondary", CLIError), - ("swap", CLIError) - ] + "req, exp", [("primary", CLIError), ("secondary", CLIError), ("swap", CLIError)] ) - def test_device_key_regenerate_invalid_args(self, fixture_cmd, serviceclient_invalid_args, req, exp): + def test_device_key_regenerate_invalid_args( + self, fixture_cmd, serviceclient_invalid_args, req, exp + ): with pytest.raises(exp): subject.iot_device_key_regenerate( fixture_cmd, mock_target["entity"], device_id, req @@ -716,33 +713,29 @@ def test_device_key_regenerate_error(self, serviceclient_generic_error, req): class TestDeviceDelete: - @pytest.fixture(params=[(200, 204)]) + @pytest.fixture(params=[204]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - etag = str(uuid4()) - service_client.expected_etag = etag test_side_effect = [ - build_mock_response(mocker, request.param[0], {"etag": etag}), - build_mock_response(mocker, request.param[1]), + build_mock_response(mocker, request.param), ] service_client.side_effect = test_side_effect return service_client - def test_device_delete(self, serviceclient): - subject.iot_device_delete(fixture_cmd, device_id, mock_target["entity"]) + @pytest.mark.parametrize("target_etag", [generate_generic_id(), None]) + def test_device_delete(self, serviceclient, target_etag): + subject.iot_device_delete( + cmd=fixture_cmd, + device_id=device_id, + hub_name=mock_target["entity"], + etag=target_etag, + ) args = serviceclient.call_args url = args[0][0].url assert "{}/devices/{}?".format(mock_target["entity"], device_id) in url assert args[0][0].method == "DELETE" headers = args[0][0].headers - assert headers["If-Match"] == '"{}"'.format(serviceclient.expected_etag) - - @pytest.mark.parametrize("exception", [CLIError]) - def test_device_delete_invalid_args( - self, serviceclient_generic_invalid_or_missing_etag, exception - ): - with pytest.raises(exception): - subject.iot_device_delete(fixture_cmd, device_id, mock_target["entity"]) + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") def test_device_delete_error(self, serviceclient_generic_error): with pytest.raises(CLIError): @@ -755,11 +748,13 @@ def test_device_show(self, fixture_cmd, mocked_response, fixture_ghcs): device_id = generate_generic_id() mocked_response.add( method=responses.GET, - url=re.compile("https://{}/devices/{}".format(mock_target["entity"], device_id)), + url=re.compile( + "https://{}/devices/{}".format(mock_target["entity"], device_id) + ), body=json.dumps(generate_device_show(deviceId=device_id)), status=200, - content_type='application/json', - match_querystring=False + content_type="application/json", + match_querystring=False, ) result = subject.iot_device_show(fixture_cmd, device_id, mock_target["entity"]) @@ -785,7 +780,7 @@ def service_client(self, mocked_response, fixture_ghcs, request): headers={"x-ms-continuation": ""}, status=200, content_type="application/json", - match_querystring=False + match_querystring=False, ) mocked_response.expected_size = size @@ -924,7 +919,8 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): authentication={ "symmetricKey": {"primaryKey": "", "secondaryKey": ""}, "type": "sas", - } + }, + etag=generate_generic_id(), ) ), ( @@ -975,8 +971,10 @@ def test_device_module_update(self, serviceclient, req): elif req["authentication"]["type"] == "selfSigned": assert body["authentication"]["x509Thumbprint"]["primaryThumbprint"] assert body["authentication"]["x509Thumbprint"]["secondaryThumbprint"] + + target_etag = req.get("etag") headers = args[0][0].headers - assert headers["If-Match"] == '"{}"'.format(req["etag"]) + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") @pytest.mark.parametrize( "req, exp", @@ -997,7 +995,6 @@ def test_device_module_update(self, serviceclient, req): generate_device_module_show(authentication={"type": "doesnotexist"}), CLIError, ), - (generate_device_module_show(etag=None), CLIError), ], ) def test_device_module_update_invalid_args(self, serviceclient, req, exp): @@ -1023,21 +1020,23 @@ def test_device_module_update_error(self, serviceclient_generic_error, req): class TestDeviceModuleDelete: - @pytest.fixture(params=[(200, 204)]) + @pytest.fixture(params=[204]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - etag = str(uuid4()) - service_client.expected_etag = etag test_side_effect = [ - build_mock_response(mocker, request.param[0], {"etag": etag}), - build_mock_response(mocker, request.param[1]), + build_mock_response(mocker, request.param), ] service_client.side_effect = test_side_effect return service_client - def test_device_module_delete(self, serviceclient): + @pytest.mark.parametrize("target_etag", [generate_generic_id(), None]) + def test_device_module_delete(self, serviceclient, target_etag): subject.iot_device_module_delete( - fixture_cmd, device_id, module_id=module_id, hub_name=mock_target["entity"] + fixture_cmd, + device_id, + module_id=module_id, + hub_name=mock_target["entity"], + etag=target_etag, ) args = serviceclient.call_args url = args[0][0].url @@ -1046,19 +1045,7 @@ def test_device_module_delete(self, serviceclient): assert "devices/{}/modules/{}?".format(device_id, module_id) in url assert method == "DELETE" - assert headers["If-Match"] == '"{}"'.format(serviceclient.expected_etag) - - @pytest.mark.parametrize("exception", [CLIError]) - def test_device_module_invalid_args( - self, serviceclient_generic_invalid_or_missing_etag, exception - ): - with pytest.raises(exception): - subject.iot_device_module_delete( - fixture_cmd, - device_id, - module_id=module_id, - hub_name=mock_target["entity"], - ) + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") def test_device_module_delete_error(self, serviceclient_generic_error): with pytest.raises(CLIError): @@ -1144,7 +1131,7 @@ def generate_device_twin_show(file_handle=False, **kvp): path = os.path.realpath("test_generic_twin.json") return path - payload = {"deviceId": device_id, "etag": "abcd"} + payload = {"deviceId": device_id} for k in kvp: payload[k] = kvp[k] return payload @@ -1187,20 +1174,32 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): # Update does a GET/SHOW first @pytest.mark.parametrize( - "req", [(generate_device_twin_show(properties={"desired": {"key": "value"}}))] + "req", + [ + generate_device_twin_show( + properties={"desired": {"key": "value"}}, etag="abcd" + ), + generate_device_twin_show(properties={"desired": {"key": "value"}}), + ], ) def test_device_twin_update(self, serviceclient, req): subject.iot_device_twin_update( - fixture_cmd, req["deviceId"], hub_name=mock_target["entity"], parameters=req + fixture_cmd, + req["deviceId"], + hub_name=mock_target["entity"], + parameters=req, + etag=req.get("etag"), ) args = serviceclient.call_args body = json.loads(args[0][0].body) assert body == req assert "twins/{}".format(device_id) in args[0][0].url - @pytest.mark.parametrize( - "req", [generate_device_twin_show()] - ) + target_etag = req.get("etag") + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") + + @pytest.mark.parametrize("req", [generate_device_twin_show()]) def test_device_twin_update_error(self, serviceclient_generic_error, req): with pytest.raises(CLIError): subject.iot_device_twin_update( @@ -1222,23 +1221,32 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): # Replace does a GET/SHOW first @pytest.mark.parametrize( - "req, isfile", + "req, isfile, etag", [ - (generate_device_twin_show(moduleId=module_id), False), + ( + generate_device_twin_show(moduleId=module_id), + False, + generate_generic_id(), + ), ( generate_device_twin_show( moduleId=module_id, properties={"desired": {"key": "value"}} ), False, + None, ), - (generate_device_twin_show(file_handle=True), True), + (generate_device_twin_show(file_handle=True), True, None), ], ) - def test_device_twin_replace(self, serviceclient, req, isfile): + def test_device_twin_replace(self, serviceclient, req, isfile, etag): if not isfile: req = json.dumps(req) subject.iot_device_twin_replace( - fixture_cmd, device_id, hub_name=mock_target["entity"], target_json=req + cmd=fixture_cmd, + device_id=device_id, + hub_name=mock_target["entity"], + target_json=req, + etag=etag, ) args = serviceclient.call_args body = json.loads(args[0][0].body) @@ -1250,22 +1258,8 @@ def test_device_twin_replace(self, serviceclient, req, isfile): assert "{}/twins/{}?".format(mock_target["entity"], device_id) in args[0][0].url assert args[0][0].method == "PUT" - @pytest.mark.parametrize( - "req, exp", - [ - (generate_device_twin_show(etag=None), CLIError), - ({"invalid": "args"}, CLIError) - ], - ) - def test_device_twin_replace_invalid_args(self, serviceclient, req, exp): - with pytest.raises(exp): - serviceclient.return_value = build_mock_response(status_code=200, payload=req) - subject.iot_device_twin_replace( - fixture_cmd, - device_id, - hub_name=mock_target["entity"], - target_json=json.dumps(req), - ) + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") @pytest.mark.parametrize("req", [(generate_device_twin_show(moduleId=module_id))]) def test_device_twin_replace_error(self, serviceclient_generic_error, req): @@ -1319,11 +1313,18 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): @pytest.mark.parametrize( "req", [ + ( + generate_device_twin_show( + moduleId=module_id, + properties={"desired": {"key": "value"}}, + etag=generate_generic_id(), + ) + ), ( generate_device_twin_show( moduleId=module_id, properties={"desired": {"key": "value"}} ) - ) + ), ], ) def test_device_module_twin_update(self, serviceclient, req): @@ -1333,6 +1334,7 @@ def test_device_module_twin_update(self, serviceclient, req): hub_name=mock_target["entity"], module_id=module_id, parameters=req, + etag=req.get("etag"), ) args = serviceclient.call_args body = json.loads(args[0][0].body) @@ -1341,6 +1343,10 @@ def test_device_module_twin_update(self, serviceclient, req): "twins/{}/modules/{}?".format(req["deviceId"], module_id) in args[0][0].url ) + target_etag = req.get("etag") + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") + @pytest.mark.parametrize("req", [(generate_device_twin_show(moduleId=module_id))]) def test_device_module_twin_update_error(self, serviceclient_generic_error, req): with pytest.raises(CLIError): @@ -1364,19 +1370,24 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): # Replace does a GET/SHOW first @pytest.mark.parametrize( - "req, isfile", + "req, isfile, etag", [ - (generate_device_twin_show(moduleId=module_id), False), + ( + generate_device_twin_show(moduleId=module_id), + False, + generate_generic_id(), + ), ( generate_device_twin_show( moduleId=module_id, properties={"desired": {"key": "value"}} ), False, + None, ), - (generate_device_twin_show(file_handle=True), True), + (generate_device_twin_show(file_handle=True), True, None), ], ) - def test_device_module_twin_replace(self, serviceclient, req, isfile): + def test_device_module_twin_replace(self, serviceclient, req, isfile, etag): if not isfile: req = json.dumps(req) subject.iot_device_module_twin_replace( @@ -1385,6 +1396,7 @@ def test_device_module_twin_replace(self, serviceclient, req, isfile): hub_name=mock_target["entity"], module_id=module_id, target_json=req, + etag=etag, ) args = serviceclient.call_args body = json.loads(args[0][0].body) @@ -1396,26 +1408,8 @@ def test_device_module_twin_replace(self, serviceclient, req, isfile): assert "twins/{}/modules/{}?".format(device_id, module_id) in args[0][0].url assert args[0][0].method == "PUT" - @pytest.mark.parametrize( - "req, exp", - [ - (generate_device_twin_show(moduleId=module_id, etag=None), CLIError), - ({"invalid": "payload"}, CLIError), - ], - ) - def test_device_module_twin_replace_invalid_args(self, mocker, serviceclient, req, exp): - with pytest.raises(exp): - serviceclient.return_value = build_mock_response( - mocker, 200, payload=req - ) - - subject.iot_device_module_twin_replace( - fixture_cmd, - device_id, - hub_name=mock_target["entity"], - module_id=module_id, - target_json=json.dumps(req), - ) + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") @pytest.mark.parametrize("req", [(generate_device_twin_show(moduleId=module_id))]) def test_device_module_twin_replace_error(self, serviceclient_generic_error, req): @@ -1469,9 +1463,7 @@ def test_query_basic(self, serviceclient, query, servresult, servtotal, top): continuation[-1] = None serviceclient.return_value = build_mock_response( - status_code=200, - payload=servresult, - headers_get_side_effect=continuation + status_code=200, payload=servresult, headers_get_side_effect=continuation ) result = subject.iot_query( @@ -1707,7 +1699,7 @@ def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): body=payload["body"], headers=payload["headers"], status=200, - match_querystring=False + match_querystring=False, ) yield (mocked_response, payload) @@ -1715,6 +1707,7 @@ def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): @pytest.fixture() def c2d_receive_ack_scenario(self, fixture_ghcs, mocked_response): from .generators import create_c2d_receive_response + payload = create_c2d_receive_response() mocked_response.add( method=responses.GET, @@ -1798,13 +1791,14 @@ def c2d_ack_abandon_scenario(self, fixture_ghcs, mocked_response): @pytest.fixture() def c2d_purge_scenario(self, fixture_ghcs, mocked_response): import json + mocked_response.add( method=responses.DELETE, url="https://{}/devices/{}/commands".format( mock_target["entity"], device_id ), body=json.dumps(c2d_purge_response), - content_type='application/json', + content_type="application/json", status=200, ) yield mocked_response @@ -1829,15 +1823,42 @@ def test_c2d_receive(self, c2d_receive_scenario): ) assert headers["IotHub-MessageLockTimeout"] == str(timeout) - assert result["properties"]["system"]["iothub-ack"] == sample_c2d_receive["headers"]["iothub-ack"] - assert result["properties"]["system"]["iothub-correlationid"] == sample_c2d_receive["headers"]["iothub-correlationid"] - assert result["properties"]["system"]["iothub-deliverycount"] == sample_c2d_receive["headers"]["iothub-deliverycount"] - assert result["properties"]["system"]["iothub-expiry"] == sample_c2d_receive["headers"]["iothub-expiry"] - assert result["properties"]["system"]["iothub-enqueuedtime"] == sample_c2d_receive["headers"]["iothub-enqueuedtime"] - assert result["properties"]["system"]["iothub-messageid"] == sample_c2d_receive["headers"]["iothub-messageid"] - assert result["properties"]["system"]["iothub-sequencenumber"] == sample_c2d_receive["headers"]["iothub-sequencenumber"] - assert result["properties"]["system"]["iothub-userid"] == sample_c2d_receive["headers"]["iothub-userid"] - assert result["properties"]["system"]["iothub-to"] == sample_c2d_receive["headers"]["iothub-to"] + assert ( + result["properties"]["system"]["iothub-ack"] + == sample_c2d_receive["headers"]["iothub-ack"] + ) + assert ( + result["properties"]["system"]["iothub-correlationid"] + == sample_c2d_receive["headers"]["iothub-correlationid"] + ) + assert ( + result["properties"]["system"]["iothub-deliverycount"] + == sample_c2d_receive["headers"]["iothub-deliverycount"] + ) + assert ( + result["properties"]["system"]["iothub-expiry"] + == sample_c2d_receive["headers"]["iothub-expiry"] + ) + assert ( + result["properties"]["system"]["iothub-enqueuedtime"] + == sample_c2d_receive["headers"]["iothub-enqueuedtime"] + ) + assert ( + result["properties"]["system"]["iothub-messageid"] + == sample_c2d_receive["headers"]["iothub-messageid"] + ) + assert ( + result["properties"]["system"]["iothub-sequencenumber"] + == sample_c2d_receive["headers"]["iothub-sequencenumber"] + ) + assert ( + result["properties"]["system"]["iothub-userid"] + == sample_c2d_receive["headers"]["iothub-userid"] + ) + assert ( + result["properties"]["system"]["iothub-to"] + == sample_c2d_receive["headers"]["iothub-to"] + ) assert result["etag"] == sample_c2d_receive["headers"]["etag"].strip('"') @@ -1855,9 +1876,9 @@ def test_c2d_receive_ack(self, c2d_receive_ack_scenario): device_id, mock_target["entity"], timeout, - complete=(ack == 'complete'), - reject=(ack == 'reject'), - abandon=(ack == 'abandon') + complete=(ack == "complete"), + reject=(ack == "reject"), + abandon=(ack == "abandon"), ) retrieve, action = service_client.calls[0], service_client.calls[1] @@ -1873,18 +1894,42 @@ def test_c2d_receive_ack(self, c2d_receive_ack_scenario): ) assert headers["IotHub-MessageLockTimeout"] == str(timeout) - assert result["properties"]["system"]["iothub-ack"] == sample_c2d_receive["headers"]["iothub-ack"] - assert result["properties"]["system"]["iothub-correlationid"] == sample_c2d_receive["headers"]["iothub-correlationid"] - assert result["properties"]["system"]["iothub-deliverycount"] == sample_c2d_receive["headers"]["iothub-deliverycount"] - assert result["properties"]["system"]["iothub-expiry"] == sample_c2d_receive["headers"]["iothub-expiry"] - assert result["properties"]["system"]["iothub-enqueuedtime"] == sample_c2d_receive["headers"]["iothub-enqueuedtime"] - assert result["properties"]["system"]["iothub-messageid"] == sample_c2d_receive["headers"]["iothub-messageid"] + assert ( + result["properties"]["system"]["iothub-ack"] + == sample_c2d_receive["headers"]["iothub-ack"] + ) + assert ( + result["properties"]["system"]["iothub-correlationid"] + == sample_c2d_receive["headers"]["iothub-correlationid"] + ) + assert ( + result["properties"]["system"]["iothub-deliverycount"] + == sample_c2d_receive["headers"]["iothub-deliverycount"] + ) + assert ( + result["properties"]["system"]["iothub-expiry"] + == sample_c2d_receive["headers"]["iothub-expiry"] + ) + assert ( + result["properties"]["system"]["iothub-enqueuedtime"] + == sample_c2d_receive["headers"]["iothub-enqueuedtime"] + ) + assert ( + result["properties"]["system"]["iothub-messageid"] + == sample_c2d_receive["headers"]["iothub-messageid"] + ) assert ( result["properties"]["system"]["iothub-sequencenumber"] == sample_c2d_receive["headers"]["iothub-sequencenumber"] ) - assert result["properties"]["system"]["iothub-userid"] == sample_c2d_receive["headers"]["iothub-userid"] - assert result["properties"]["system"]["iothub-to"] == sample_c2d_receive["headers"]["iothub-to"] + assert ( + result["properties"]["system"]["iothub-userid"] + == sample_c2d_receive["headers"]["iothub-userid"] + ) + assert ( + result["properties"]["system"]["iothub-to"] + == sample_c2d_receive["headers"]["iothub-to"] + ) assert result["etag"] == sample_c2d_receive["headers"]["etag"].strip('"') @@ -2024,9 +2069,10 @@ def test_c2d_message_purge(self, c2d_purge_scenario): method = request.method assert method == "DELETE" - assert "https://{}/devices/{}/commands".format( - mock_target["entity"], device_id - ) in url + assert ( + "https://{}/devices/{}/commands".format(mock_target["entity"], device_id) + in url + ) assert result assert result.total_messages_purged == 3 assert result.device_id == device_id @@ -2506,10 +2552,9 @@ def test_device_children_add(self, sc_addchildren): 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")] - ) + assert body["deviceScope"] == generate_parent_device().get( + "deviceScope" + ) or body["parentScopes"] == [generate_parent_device().get("deviceScope")] @pytest.fixture(params=[(200, 0), (200, 1)]) def sc_invalid_args_addchildren(self, mocker, fixture_ghcs, fixture_sas, request): @@ -2588,9 +2633,7 @@ def sc_listchildren(self, mocker, fixture_ghcs, fixture_sas, request): result.append(generate_child_device(**child_kvp)) test_side_effect = [ build_mock_response(mocker, 200, generate_parent_device()), - build_mock_response( - mocker, 200, result, {"x-ms-continuation": None} - ), + build_mock_response(mocker, 200, result, {"x-ms-continuation": None}), ] service_client.side_effect = test_side_effect return service_client From 6cb3b54d638c4538b4dc0d7a155a673eef634885 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Tue, 8 Dec 2020 14:02:50 -0800 Subject: [PATCH 159/179] Re-introduce pnp runtime/IoT Hub digital twin device commands. (#288) * Re-introduce pnp runtime/IoT Hub digital twin device commands. * Removes one-off PnP SDK and config as the recently updated IoT Hub service SDK contains proper endpoints. * Places runtime commands under 'az iot hub digital-twin *' * Update HISTORY.rst --- HISTORY.rst | 10 + azext_iot/_factory.py | 12 - azext_iot/common/shared.py | 1 - azext_iot/digicert.pem | 33 -- azext_iot/iothub/_help.py | 22 +- azext_iot/iothub/command_map.py | 4 +- azext_iot/iothub/commands_pnp_runtime.py | 9 +- azext_iot/iothub/params.py | 17 +- azext_iot/iothub/providers/pnp_runtime.py | 22 +- azext_iot/sdk/iothub/pnp_runtime/__init__.py | 18 - .../iot_hub_gateway_service_ap_is.py | 91 ----- .../sdk/iothub/pnp_runtime/models/__init__.py | 10 - .../iothub/pnp_runtime/operations/__init__.py | 17 - .../operations/digital_twin_operations.py | 332 ------------------ azext_iot/sdk/iothub/pnp_runtime/version.py | 13 - .../pnp_runtime/test_iot_pnp_runtime_unit.py | 96 +++-- 16 files changed, 121 insertions(+), 586 deletions(-) delete mode 100644 azext_iot/sdk/iothub/pnp_runtime/__init__.py delete mode 100644 azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py delete mode 100644 azext_iot/sdk/iothub/pnp_runtime/models/__init__.py delete mode 100644 azext_iot/sdk/iothub/pnp_runtime/operations/__init__.py delete mode 100644 azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py delete mode 100644 azext_iot/sdk/iothub/pnp_runtime/version.py diff --git a/HISTORY.rst b/HISTORY.rst index b15e2656a..fee1c9ee3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,10 @@ Release History 0.10.8 +++++++++++++++ +**IoT Central updates** + +* az iot central device|device-template|api-token|diagnostic help strings updated with improved language. + **IoT Hub updates** The following commands support an explicit etag parameter. If no etag arg is passed the value "*" is used. @@ -24,6 +28,12 @@ The following commands support an explicit etag parameter. If no etag arg is pas * az iot edge deployment update * az iot edge deployment update +Re-introduce prior in-preview IoT Hub device digital twin/pnp runtime commands under the "az iot hub digital-twin" root command group. + +* az iot hub digital-twin show +* az iot hub digital-twin update +* az iot hub digital-twin invoke-command + 0.10.7 +++++++++++++++ diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py index a64cc0901..9915c410e 100644 --- a/azext_iot/_factory.py +++ b/azext_iot/_factory.py @@ -76,7 +76,6 @@ def _construct_sdk_map(self): return { SdkType.service_sdk: self._get_iothub_service_sdk, # Don't need to call here SdkType.device_sdk: self._get_iothub_device_sdk, - SdkType.pnp_sdk: self._get_pnp_runtime_sdk, SdkType.dps_sdk: self._get_dps_service_sdk, } @@ -106,17 +105,6 @@ def _get_iothub_service_sdk(self): return IotHubGatewayServiceAPIs(credentials=credentials, base_url=self.endpoint) - def _get_pnp_runtime_sdk(self): - from azext_iot.sdk.iothub.pnp_runtime import IotHubGatewayServiceAPIs - - 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) - def _get_dps_service_sdk(self): from azext_iot.sdk.dps.service import ProvisioningServiceClient diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py index 3754e8c0d..2fd07e353 100644 --- a/azext_iot/common/shared.py +++ b/azext_iot/common/shared.py @@ -25,7 +25,6 @@ class SdkType(Enum): dps_sdk = 5 device_sdk = 6 service_sdk = 7 - pnp_sdk = 8 class EntityStatusType(Enum): diff --git a/azext_iot/digicert.pem b/azext_iot/digicert.pem index 62f06e60c..52f7643cf 100644 --- a/azext_iot/digicert.pem +++ b/azext_iot/digicert.pem @@ -75,36 +75,3 @@ zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y Johw1+qRzT65ysCQblrGXnRl11z+o+I= -----END CERTIFICATE----- -/*WoSign*/ ------BEGIN CERTIFICATE----- -MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBV -MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV -BAMTIUNlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgw -MTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFX -b1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvcqN -rLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1U -fcIiePyOCbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcScc -f+Hb0v1naMQFXQoOXXDX2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2 -ZjC1vt7tj/id07sBMOby8w7gLJKA84X5KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4M -x1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR+ScPewavVIMYe+HdVHpR -aG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ezEC8wQjch -zDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDar -uHqklWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221K -mYo0SLwX3OSACCK28jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvA -Sh0JWzko/amrzgD5LkhLJuYwTKVYyrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWv -HYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0CAwEAAaNCMEAwDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R8bNLtwYgFP6H -EtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1 -LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJ -MuYhOZO9sxXqT2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2e -JXLOC62qx1ViC777Y7NhRCOjy+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VN -g64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC2nz4SNAzqfkHx5Xh9T71XXG68pWp -dIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes5cVAWubXbHssw1ab -R80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/EaEQ -PkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGce -xGATVdVhmVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+ -J7x6v+Db9NpSvd4MVHAxkUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMl -OtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGikpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWT -ee5Ehr7XHuQe+w== ------END CERTIFICATE----- \ No newline at end of file diff --git a/azext_iot/iothub/_help.py b/azext_iot/iothub/_help.py index a23ee344c..4ee5c1e7c 100644 --- a/azext_iot/iothub/_help.py +++ b/azext_iot/iothub/_help.py @@ -92,37 +92,36 @@ def load_iothub_help(): az iot hub job cancel --hub-name {iothub_name} --job-id {job_id} """ - ''' - helps["iot pnp twin"] = """ + helps["iot hub digital-twin"] = """ type: group short-summary: Manipulate and interact with the digital twin of an IoT Hub device. """ - helps["iot pnp twin invoke-command"] = """ + helps["iot hub digital-twin invoke-command"] = """ type: command short-summary: Invoke a root or component level command of a digital twin device. examples: - name: Invoke root level command "reboot" which takes a payload that includes the "delay" property. text: > - az iot pnp twin invoke-command --command-name reboot -n {iothub_name} -d {device_id} --payload '{"delay":5}' + az iot hub digital-twin invoke-command --command-name reboot -n {iothub_name} -d {device_id} --payload '{"delay":5}' - name: Invoke command "getMaxMinReport" on component "thermostat1" that takes no input. text: > - az iot pnp twin invoke-command --cn getMaxMinReport -n {iothub_name} -d {device_id} --component-path thermostat1 + az iot hub digital-twin invoke-command --cn getMaxMinReport -n {iothub_name} -d {device_id} --component-path thermostat1 """ - helps["iot pnp twin show"] = """ + helps["iot hub digital-twin show"] = """ type: command short-summary: Show the digital twin of an IoT Hub device. examples: - name: Show the target device digital twin. text: > - az iot pnp twin show -n {iothub_name} -d {device_id} + az iot hub digital-twin show -n {iothub_name} -d {device_id} """ - helps["iot pnp twin update"] = """ + helps["iot hub digital-twin update"] = """ type: command short-summary: Update the read-write properties of a digital twin device via JSON patch specification. long-summary: Currently operations are limited to add, replace and remove. @@ -130,12 +129,12 @@ def load_iothub_help(): examples: - name: Update a digital twin via JSON patch specification. text: > - az iot pnp twin update --hub-name {iothub_name} --device-id {device_id} + az iot hub digital-twin update --hub-name {iothub_name} --device-id {device_id} --json-patch '{"op":"add", "path":"/thermostat1/targetTemperature", "value": 54}' - name: Update a digital twin via JSON patch specification. text: > - az iot pnp twin update -n {iothub_name} -d {device_id} + az iot hub digital-twin update -n {iothub_name} -d {device_id} --json-patch '[ {"op":"remove", "path":"/thermostat1/targetTemperature"}, {"op":"add", "path":"/thermostat2/targetTemperature", "value": 22} @@ -143,7 +142,6 @@ def load_iothub_help(): - name: Update a digital twin property via JSON patch specification defined in a file. text: > - az iot pnp twin update -n {iothub_name} -d {device_id} + az iot hub digital-twin update -n {iothub_name} -d {device_id} --json-patch ./my/patch/document.json """ - ''' diff --git a/azext_iot/iothub/command_map.py b/azext_iot/iothub/command_map.py index c899461da..c74a85fe2 100644 --- a/azext_iot/iothub/command_map.py +++ b/azext_iot/iothub/command_map.py @@ -25,9 +25,7 @@ def load_iothub_commands(self, _): cmd_group.command("list", "job_list") cmd_group.command("cancel", "job_cancel") - ''' - with self.command_group("iot pnp twin", command_type=pnp_runtime_ops) as cmd_group: + with self.command_group("iot hub digital-twin", command_type=pnp_runtime_ops) as cmd_group: cmd_group.command("invoke-command", "invoke_device_command") cmd_group.show_command("show", "get_digital_twin") cmd_group.command("update", "patch_digital_twin") - ''' diff --git a/azext_iot/iothub/commands_pnp_runtime.py b/azext_iot/iothub/commands_pnp_runtime.py index eba7a2f70..83a608f2f 100644 --- a/azext_iot/iothub/commands_pnp_runtime.py +++ b/azext_iot/iothub/commands_pnp_runtime.py @@ -14,9 +14,10 @@ def invoke_device_command( cmd, device_id, command_name, - timeout=30, component_path=None, payload="{}", + connect_timeout=None, + response_timeout=None, hub_name=None, resource_group_name=None, login=None, @@ -29,7 +30,8 @@ def invoke_device_command( command_name=command_name, payload=payload, component_path=component_path, - timeout=timeout, + connect_timeout=connect_timeout, + response_timeout=response_timeout ) @@ -55,10 +57,11 @@ def patch_digital_twin( hub_name=None, resource_group_name=None, login=None, + etag=None ): runtime_provider = PnPRuntimeProvider( cmd=cmd, hub_name=hub_name, rg=resource_group_name, login=login ) return runtime_provider.patch_digital_twin( - device_id=device_id, json_patch=json_patch + device_id=device_id, json_patch=json_patch, etag=etag ) diff --git a/azext_iot/iothub/params.py b/azext_iot/iothub/params.py index f1beee189..1265ae7b5 100644 --- a/azext_iot/iothub/params.py +++ b/azext_iot/iothub/params.py @@ -4,11 +4,12 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- + def load_iothub_arguments(self, _): """ Load CLI Args for Knack parser """ - with self.argument_context("iot pnp twin") as context: + with self.argument_context("iot hub digital-twin") as context: context.argument( "command_name", options_list=["--command-name", "--cn"], @@ -30,3 +31,17 @@ def load_iothub_arguments(self, _): options_list=["--payload"], help="JSON payload input for command. Provide file path or inline JSON.", ) + context.argument( + "connect_timeout", + type=int, + options_list=["--connect-timeout", "--cto"], + help="Maximum interval of time, in seconds, that IoT Hub will attempt to connect to the device.", + arg_group="Timeout" + ) + context.argument( + "response_timeout", + type=int, + options_list=["--response-timeout", "--rto"], + help="Maximum interval of time, in seconds, that the digital twin command will wait for the result.", + arg_group="Timeout" + ) diff --git a/azext_iot/iothub/providers/pnp_runtime.py b/azext_iot/iothub/providers/pnp_runtime.py index f99bcc09e..20c827d2b 100644 --- a/azext_iot/iothub/providers/pnp_runtime.py +++ b/azext_iot/iothub/providers/pnp_runtime.py @@ -13,6 +13,7 @@ IoTHubProvider, CloudError, ) +from azext_iot.sdk.iothub.service.operations.digital_twin_operations import DigitalTwinOperations logger = get_logger(__name__) @@ -23,19 +24,19 @@ def __init__(self, cmd, hub_name=None, rg=None, login=None): super(PnPRuntimeProvider, self).__init__( cmd=cmd, hub_name=hub_name, rg=rg, login=login ) - self.runtime_sdk = self.get_sdk(SdkType.pnp_sdk).digital_twin + self.runtime_sdk: DigitalTwinOperations = self.get_sdk( + SdkType.service_sdk + ).digital_twin def invoke_device_command( self, device_id, command_name, - timeout=30, payload="{}", component_path=None, connect_timeout=None, response_timeout=None, ): - # Prevent msrest locking up shell self.runtime_sdk.config.retry_policy.retries = 1 @@ -44,15 +45,15 @@ def invoke_device_command( payload = process_json_arg(payload, argument_name="payload") api_timeout_kwargs = { - "connect_timeout_in_seconds": timeout, - "response_timeout_in_seconds": timeout, + "connect_timeout_in_seconds": connect_timeout, + "response_timeout_in_seconds": response_timeout, } response = ( self.runtime_sdk.invoke_component_command( id=device_id, command_name=command_name, payload=payload, - timeout=timeout, + timeout=connect_timeout, component_path=component_path, raw=True, **api_timeout_kwargs, @@ -62,7 +63,7 @@ def invoke_device_command( id=device_id, command_name=command_name, payload=payload, - timeout=timeout, + timeout=connect_timeout, raw=True, **api_timeout_kwargs, ).response @@ -79,7 +80,7 @@ def invoke_device_command( def get_digital_twin(self, device_id): return self.runtime_sdk.get_digital_twin(id=device_id, raw=True).response.json() - def patch_digital_twin(self, device_id, json_patch): + def patch_digital_twin(self, device_id, json_patch, etag=None): json_patch = process_json_arg(content=json_patch, argument_name="json-patch") json_patch_collection = [] @@ -93,7 +94,10 @@ def patch_digital_twin(self, device_id, json_patch): try: # Currently no response text is returned from the update self.runtime_sdk.update_digital_twin( - id=device_id, digital_twin_patch=json_patch_collection, if_match="*", raw=True + id=device_id, + digital_twin_patch=json_patch_collection, + if_match=etag if etag else "*", + raw=True, ).response return self.get_digital_twin(device_id=device_id) diff --git a/azext_iot/sdk/iothub/pnp_runtime/__init__.py b/azext_iot/sdk/iothub/pnp_runtime/__init__.py deleted file mode 100644 index 41715539b..000000000 --- a/azext_iot/sdk/iothub/pnp_runtime/__init__.py +++ /dev/null @@ -1,18 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .iot_hub_gateway_service_ap_is import IotHubGatewayServiceAPIs -from .version import VERSION - -__all__ = ['IotHubGatewayServiceAPIs'] - -__version__ = VERSION - diff --git a/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py b/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py deleted file mode 100644 index e5f7a1273..000000000 --- a/azext_iot/sdk/iothub/pnp_runtime/iot_hub_gateway_service_ap_is.py +++ /dev/null @@ -1,91 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from msrest.service_client import SDKClient -from msrest import Serializer, Deserializer -from msrestazure import AzureConfiguration -from .version import VERSION -from .operations.digital_twin_operations import DigitalTwinOperations -from azext_iot.constants import USER_AGENT -from . import models - - -class IotHubGatewayServiceAPIsConfiguration(AzureConfiguration): - """Configuration for IotHubGatewayServiceAPIs - Note that all parameters used to create this instance are saved as instance - attributes. - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - if credentials is None: - raise ValueError("Parameter 'credentials' must not be None.") - if not base_url: - base_url = 'https://fully-qualified-iothubname.azure-devices.net' - - super(IotHubGatewayServiceAPIsConfiguration, self).__init__(base_url) - - self.add_user_agent('iothubgatewayserviceapis/{}'.format(VERSION)) - self.add_user_agent(USER_AGENT) # @digimaun - - self.credentials = credentials - - -class IotHubGatewayServiceAPIs(SDKClient): - """IotHubGatewayServiceAPIs - - :ivar config: Configuration for client. - :vartype config: IotHubGatewayServiceAPIsConfiguration - - :ivar configuration: Configuration operations - :vartype configuration: pnp_runtime.operations.ConfigurationOperations - :ivar registry_manager: RegistryManager operations - :vartype registry_manager: pnp_runtime.operations.RegistryManagerOperations - :ivar job_client: JobClient operations - :vartype job_client: pnp_runtime.operations.JobClientOperations - :ivar fault_injection: FaultInjection operations - :vartype fault_injection: pnp_runtime.operations.FaultInjectionOperations - :ivar twin: Twin operations - :vartype twin: pnp_runtime.operations.TwinOperations - :ivar digital_twin: DigitalTwin operations - :vartype digital_twin: pnp_runtime.operations.DigitalTwinOperations - :ivar http_runtime: HttpRuntime operations - :vartype http_runtime: pnp_runtime.operations.HttpRuntimeOperations - :ivar module_api: ModuleApi operations - :vartype module_api: pnp_runtime.operations.ModuleApiOperations - :ivar device_method: DeviceMethod operations - :vartype device_method: pnp_runtime.operations.DeviceMethodOperations - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param str base_url: Service URL - """ - - def __init__( - self, credentials, base_url=None): - - self.config = IotHubGatewayServiceAPIsConfiguration(credentials, base_url) - super(IotHubGatewayServiceAPIs, self).__init__(self.config.credentials, self.config) - - client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2020-09-30' - self._serialize = Serializer(client_models) - self._deserialize = Deserializer(client_models) - - self.digital_twin = DigitalTwinOperations( - self._client, self.config, self._serialize, self._deserialize) diff --git a/azext_iot/sdk/iothub/pnp_runtime/models/__init__.py b/azext_iot/sdk/iothub/pnp_runtime/models/__init__.py deleted file mode 100644 index 593333085..000000000 --- a/azext_iot/sdk/iothub/pnp_runtime/models/__init__.py +++ /dev/null @@ -1,10 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- diff --git a/azext_iot/sdk/iothub/pnp_runtime/operations/__init__.py b/azext_iot/sdk/iothub/pnp_runtime/operations/__init__.py deleted file mode 100644 index 0de995247..000000000 --- a/azext_iot/sdk/iothub/pnp_runtime/operations/__init__.py +++ /dev/null @@ -1,17 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -from .digital_twin_operations import DigitalTwinOperations - - -__all__ = [ - 'DigitalTwinOperations', -] diff --git a/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py b/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py deleted file mode 100644 index 9c66420a2..000000000 --- a/azext_iot/sdk/iothub/pnp_runtime/operations/digital_twin_operations.py +++ /dev/null @@ -1,332 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -import uuid -from msrest.pipeline import ClientRawResponse -from msrestazure.azure_exceptions import CloudError - -# @digimaun - removed models references as no models are used. - - -class DigitalTwinOperations(object): - """DigitalTwinOperations operations. - - :param client: Client for service requests. - :param config: Configuration of service client. - :param serializer: An object model serializer. - :param deserializer: An object model deserializer. - :ivar api_version: The API version to use for the request. Constant value: "2020-09-30". - """ - - def __init__(self, client, config, serializer, deserializer): - - self._client = client - self._serialize = serializer - self._deserialize = deserializer - self.api_version = "2020-09-30" - - self.config = config - - def get_digital_twin( - self, id, custom_headers=None, raw=False, **operation_config): - """Gets a digital twin. - - :param id: Digital Twin ID. - :type id: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.get_digital_twin.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('object', response) - header_dict = { - 'ETag': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - get_digital_twin.metadata = {'url': '/digitaltwins/{id}'} - - def update_digital_twin( - self, id, digital_twin_patch, if_match=None, custom_headers=None, raw=False, **operation_config): - """Updates a digital twin. - - :param id: Digital Twin ID. - :type id: str - :param digital_twin_patch: json-patch contents to update. - :type digital_twin_patch: list[object] - :param if_match: - :type if_match: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.update_digital_twin.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - - # Construct headers - header_parameters = {} - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if if_match is not None: - header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str') - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(digital_twin_patch, '[object]') - - # Construct and send request - request = self._client.patch(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [202]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - if raw: - client_raw_response = ClientRawResponse(None, response) - client_raw_response.add_headers({ - 'ETag': 'str', - 'Location': 'str', - }) - return client_raw_response - update_digital_twin.metadata = {'url': '/digitaltwins/{id}'} - - def invoke_root_level_command( - self, id, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config): - """Invoke a digital twin root level command. - - Invoke a digital twin root level command. - - :param id: - :type id: str - :param command_name: - :type command_name: str - :param payload: - :type payload: object - :param connect_timeout_in_seconds: Maximum interval of time, in - seconds, that the digital twin command will wait for the answer. - :type connect_timeout_in_seconds: int - :param response_timeout_in_seconds: Maximum interval of time, in - seconds, that the digital twin command will wait for the answer. - :type response_timeout_in_seconds: int - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.invoke_root_level_command.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'commandName': self._serialize.url("command_name", command_name, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - if connect_timeout_in_seconds is not None: - query_parameters['connectTimeoutInSeconds'] = self._serialize.query("connect_timeout_in_seconds", connect_timeout_in_seconds, 'int') - if response_timeout_in_seconds is not None: - query_parameters['responseTimeoutInSeconds'] = self._serialize.query("response_timeout_in_seconds", response_timeout_in_seconds, 'int') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(payload, 'object') - - # Construct and send request - request = self._client.post(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('object', response) - header_dict = { - 'x-ms-command-statuscode': 'int', - 'x-ms-request-id': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - invoke_root_level_command.metadata = {'url': '/digitaltwins/{id}/commands/{commandName}'} - - def invoke_component_command( - self, id, component_path, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config): - """Invoke a digital twin command. - - Invoke a digital twin command. - - :param id: - :type id: str - :param component_path: - :type component_path: str - :param command_name: - :type command_name: str - :param payload: - :type payload: object - :param connect_timeout_in_seconds: Maximum interval of time, in - seconds, that the digital twin command will wait for the answer. - :type connect_timeout_in_seconds: int - :param response_timeout_in_seconds: Maximum interval of time, in - seconds, that the digital twin command will wait for the answer. - :type response_timeout_in_seconds: int - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: object or ClientRawResponse if raw=true - :rtype: object or ~msrest.pipeline.ClientRawResponse - :raises: :class:`CloudError` - """ - # Construct URL - url = self.invoke_component_command.metadata['url'] - path_format_arguments = { - 'id': self._serialize.url("id", id, 'str'), - 'componentPath': self._serialize.url("component_path", component_path, 'str'), - 'commandName': self._serialize.url("command_name", command_name, 'str') - } - url = self._client.format_url(url, **path_format_arguments) - - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') - if connect_timeout_in_seconds is not None: - query_parameters['connectTimeoutInSeconds'] = self._serialize.query("connect_timeout_in_seconds", connect_timeout_in_seconds, 'int') - if response_timeout_in_seconds is not None: - query_parameters['responseTimeoutInSeconds'] = self._serialize.query("response_timeout_in_seconds", response_timeout_in_seconds, 'int') - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(payload, 'object') - - # Construct and send request - request = self._client.post(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) - - if response.status_code not in [200]: - exp = CloudError(response) - exp.request_id = response.headers.get('x-ms-request-id') - raise exp - - deserialized = None - header_dict = {} - - if response.status_code == 200: - deserialized = self._deserialize('object', response) - header_dict = { - 'x-ms-command-statuscode': 'int', - 'x-ms-request-id': 'str', - } - - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - client_raw_response.add_headers(header_dict) - return client_raw_response - - return deserialized - invoke_component_command.metadata = {'url': '/digitaltwins/{id}/components/{componentPath}/commands/{commandName}'} diff --git a/azext_iot/sdk/iothub/pnp_runtime/version.py b/azext_iot/sdk/iothub/pnp_runtime/version.py deleted file mode 100644 index f4d73cc97..000000000 --- a/azext_iot/sdk/iothub/pnp_runtime/version.py +++ /dev/null @@ -1,13 +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. -# -# Code generated by Microsoft (R) AutoRest Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is -# regenerated. -# -------------------------------------------------------------------------- - -VERSION = "2020-09-30" - diff --git a/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py b/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py index 11c9e7bf6..e36fa1f96 100644 --- a/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py +++ b/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py @@ -59,17 +59,21 @@ def service_client_component(self, mocked_response, fixture_ghcs, request): yield mocked_response @pytest.mark.parametrize( - "request_payload", [("{}"), (json.dumps({"key": str(generate_generic_id())}))], + "request_payload, arbitrary_timeout", + [ + ("{}", randint(10, 30)), + (json.dumps({"key": str(generate_generic_id())}), None), + ], ) def test_pnp_runtime_invoke_root_command( - self, fixture_cmd, service_client_root, request_payload + self, fixture_cmd, service_client_root, request_payload, arbitrary_timeout ): - arbitrary_timeout_int = randint(10, 30) result = subject.invoke_device_command( cmd=fixture_cmd, device_id=device_id, command_name=command_id, - timeout=arbitrary_timeout_int, + connect_timeout=arbitrary_timeout, + response_timeout=arbitrary_timeout, payload=request_payload, hub_name=mock_target["entity"], ) @@ -78,21 +82,26 @@ def test_pnp_runtime_invoke_root_command( request_payload=request_payload, executed_client=service_client_root, result=result, - timeout=arbitrary_timeout_int, + connect_timeout=arbitrary_timeout, + response_timeout=arbitrary_timeout, ) @pytest.mark.parametrize( - "request_payload", [("{}"), (json.dumps({"key": str(generate_generic_id())}))], + "request_payload, arbitrary_timeout", + [ + ("{}", randint(10, 30)), + (json.dumps({"key": str(generate_generic_id())}), None), + ], ) def test_pnp_runtime_invoke_component_command( - self, fixture_cmd, service_client_component, request_payload + self, fixture_cmd, service_client_component, request_payload, arbitrary_timeout ): - arbitrary_timeout_int = randint(10, 30) result = subject.invoke_device_command( cmd=fixture_cmd, device_id=device_id, command_name=command_id, - timeout=arbitrary_timeout_int, + connect_timeout=arbitrary_timeout, + response_timeout=arbitrary_timeout, payload=request_payload, hub_name=mock_target["entity"], component_path=component_id, @@ -102,18 +111,20 @@ def test_pnp_runtime_invoke_component_command( request_payload=request_payload, executed_client=service_client_component, result=result, - timeout=arbitrary_timeout_int, + connect_timeout=arbitrary_timeout, + response_timeout=arbitrary_timeout, ) def test_pnp_runtime_invoke_command_error( - self, fixture_cmd, service_client_generic_errors, + self, + fixture_cmd, + service_client_generic_errors, ): with pytest.raises(CLIError): subject.invoke_device_command( cmd=fixture_cmd, device_id=device_id, command_name=command_id, - timeout=10, payload=json.dumps({}), hub_name=mock_target["entity"], ) @@ -123,23 +134,30 @@ def test_pnp_runtime_invoke_command_error( cmd=fixture_cmd, device_id=device_id, command_name=command_id, - timeout=10, payload=json.dumps({}), hub_name=mock_target["entity"], component_path=component_id, ) def _assert_common_attributes( - self, request_payload, executed_client, result, timeout=None + self, + request_payload, + executed_client, + result, + connect_timeout=None, + response_timeout=None, ): - assert ( - "connectTimeoutInSeconds={}".format(timeout) - in executed_client.calls[0].request.url - ) - assert ( - "responseTimeoutInSeconds={}".format(timeout) - in executed_client.calls[0].request.url - ) + if connect_timeout: + assert ( + "connectTimeoutInSeconds={}".format(connect_timeout) + in executed_client.calls[0].request.url + ) + + if response_timeout: + assert ( + "responseTimeoutInSeconds={}".format(response_timeout) + in executed_client.calls[0].request.url + ) assert request_payload == executed_client.calls[0].request.body assert ( @@ -156,7 +174,10 @@ class TestPnPRuntimeShowDigitalTwin(object): def service_client(self, mocked_response, fixture_ghcs, request): mocked_response.add( method=responses.GET, - url="https://{}/digitaltwins/{}".format(mock_target["entity"], device_id,), + url="https://{}/digitaltwins/{}".format( + mock_target["entity"], + device_id, + ), body=generic_result, status=200, content_type="application/json", @@ -167,7 +188,9 @@ def service_client(self, mocked_response, fixture_ghcs, request): def test_pnp_runtime_show_digital_twin(self, fixture_cmd, service_client): result = subject.get_digital_twin( - cmd=fixture_cmd, device_id=device_id, hub_name=mock_target["entity"], + cmd=fixture_cmd, + device_id=device_id, + hub_name=mock_target["entity"], ) # Validates simple endpoint behavior. @@ -180,7 +203,10 @@ class TestPnPRuntimeUpdateDigitalTwin(object): def service_client(self, mocked_response, fixture_ghcs, request): mocked_response.add( method=responses.PATCH, - url="https://{}/digitaltwins/{}".format(mock_target["entity"], device_id,), + url="https://{}/digitaltwins/{}".format( + mock_target["entity"], + device_id, + ), body=None, status=202, content_type="application/json", @@ -191,7 +217,10 @@ def service_client(self, mocked_response, fixture_ghcs, request): # because the update operation does not return anything. mocked_response.add( method=responses.GET, - url="https://{}/digitaltwins/{}".format(mock_target["entity"], device_id,), + url="https://{}/digitaltwins/{}".format( + mock_target["entity"], + device_id, + ), body=generic_result, status=200, content_type="application/json", @@ -201,17 +230,21 @@ def service_client(self, mocked_response, fixture_ghcs, request): yield mocked_response @pytest.mark.parametrize( - "request_json_patch", + "request_json_patch, etag", [ ( '[{"op":"remove", "path":"/thermostat1/targetTemperature"}, ' - '{"op":"add", "path":"/thermostat2/targetTemperature", "value": 22}]' + '{"op":"add", "path":"/thermostat2/targetTemperature", "value": 22}]', + generate_generic_id(), + ), + ( + '{"op":"add", "path":"/thermostat1/targetTemperature", "value": 54}', + None, ), - ('{"op":"add", "path":"/thermostat1/targetTemperature", "value": 54}'), ], ) def test_pnp_runtime_update_digital_twin( - self, fixture_cmd, service_client, request_json_patch + self, fixture_cmd, service_client, request_json_patch, etag ): json_patch = json.loads(request_json_patch) @@ -228,12 +261,13 @@ def test_pnp_runtime_update_digital_twin( device_id=device_id, hub_name=mock_target["entity"], json_patch=request_json_patch, + etag=etag, ) # First call for patch patch_request = service_client.calls[0].request assert patch_request.body == expected_request_body - assert patch_request.headers["If-Match"] == "*" + assert patch_request.headers["If-Match"] == etag if etag else "*" # Result from get twin assert result == json.loads(generic_result) From 88ebac6fd5f9c1bb6a3c6708048796dae56965aa Mon Sep 17 00:00:00 2001 From: valluriraj Date: Tue, 15 Dec 2020 16:06:12 -0800 Subject: [PATCH 160/179] Add dtdlv2 support (#289) * Update parsing template logic to support DTDLV2 models. * Remove deprecated commands 1) iot central app device-twin 2) iot central app monitor-events * Resolve PR feedback --- HISTORY.rst | 3 + azext_iot/central/_help.py | 64 +------- azext_iot/central/command_map.py | 25 ---- azext_iot/central/models/template.py | 79 ++++++++-- azext_iot/central/params.py | 12 -- azext_iot/constants.py | 2 +- azext_iot/monitor/parsers/central_parser.py | 45 ++++-- azext_iot/monitor/parsers/strings.py | 17 ++- azext_iot/monitor/property.py | 61 ++++---- .../central/json/deeply_nested_template.json | Bin 35180 -> 35148 bytes .../tests/central/json/device_template.json | 4 +- .../json/device_template_int_test.json | 11 +- .../json/property_validation_template.json | 125 +++++++++++++++- azext_iot/tests/test_iot_central_int.py | 57 +------ azext_iot/tests/test_iot_central_unit.py | 110 ++++++++------ .../tests/test_iot_central_validator_unit.py | 63 ++++++++ azext_iot/tests/test_monitor_parsers_unit.py | 140 ++++++++++++++++++ 17 files changed, 551 insertions(+), 267 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index fee1c9ee3..289c87a00 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,9 @@ Release History **IoT Central updates** * az iot central device|device-template|api-token|diagnostic help strings updated with improved language. +* update parsing template logic to support DTDLV2 models. +* remove deprecated commands 1) iot central app device-twin 2) iot central app monitor-events + **IoT Hub updates** diff --git a/azext_iot/central/_help.py b/azext_iot/central/_help.py index 98cfc38de..05ecf8159 100644 --- a/azext_iot/central/_help.py +++ b/azext_iot/central/_help.py @@ -39,8 +39,6 @@ def load_central_help(): _load_central_monitors_help() _load_central_command_help() _load_central_compute_device_key() - # TODO: Delete this by end of Dec 2020 - _load_central_deprecated_commands() def _load_central_devices_help(): @@ -528,23 +526,6 @@ def _load_central_monitors_help(): az iot central diagnostics registration-summary --app-id {appid} """ - -# TODO: Delete this by end of Dec 2020 -def _load_central_deprecated_commands(): - helps[ - "iot central app device-twin" - ] = """ - type: group - short-summary: Manage IoT Central device twins. - """ - - helps[ - "iot central app device-twin show" - ] = """ - type: command - short-summary: Get the device twin from IoT Hub. - """ - helps[ "iot central device twin" ] = """ @@ -556,48 +537,5 @@ def _load_central_deprecated_commands(): "iot central device twin show" ] = """ type: command - short-summary: Get the device twin from IoT Central application. - long-summary: Returns back the desired and reported device properties from the IoT Central application. - """ - - helps[ - "iot central app monitor-events" - ] = """ - type: command - short-summary: Monitor device telemetry & messages sent to the IoT Hub for an IoT Central app. - long-summary: | - EXPERIMENTAL requires Python 3.5+ - This command relies on and may install dependent Cython package (uamqp) upon first execution. - https://github.com/Azure/azure-uamqp-python - examples: - - name: Basic usage - text: > - az iot central app monitor-events --app-id {app_id} - - name: Basic usage when filtering on target device - text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} - - name: Basic usage when filtering targeted devices with a wildcard in the ID - text: > - az iot central app monitor-events --app-id {app_id} -d Device*d - - name: Basic usage when filtering on module. - text: > - az iot central app monitor-events --app-id {app_id} -m {module_id} - - name: Basic usage when filtering targeted modules with a wildcard in the ID - text: > - az iot central app monitor-events --app-id {app_id} -m Module* - - name: Filter device and specify an Event Hub consumer group to bind to. - text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} --cg {consumer_group_name} - - name: Receive message annotations (message headers) - text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno - - name: Receive message annotations + system properties. Never time out. - text: > - az iot central app monitor-events --app-id {app_id} -d {device_id} --properties anno sys --timeout 0 - - name: Receive all message attributes from all device messages - text: > - az iot central app monitor-events --app-id {app_id} --props all - - name: Receive all messages and parse message payload as JSON - text: > - az iot central app monitor-events --app-id {app_id} --output json + short-summary: Get the device twin from IoT Hub. """ diff --git a/azext_iot/central/command_map.py b/azext_iot/central/command_map.py index 9c5579616..42c092bd1 100644 --- a/azext_iot/central/command_map.py +++ b/azext_iot/central/command_map.py @@ -109,28 +109,3 @@ def load_central_commands(self, _): cmd_group.show_command( "show", "device_twin_show", ) - - # TODO: Delete this by end of Dec 2020 - _load_central_deprecated_commands(self, _) - - -def _load_central_deprecated_commands(self, _): - with self.command_group( - "iot central app device-twin", - command_type=central_device_twin_ops, - deprecate_info=self.deprecate(redirect="iot central device twin"), - ) as cmd_group: - cmd_group.show_command( - "show", "device_twin_show", - ) - - with self.command_group( - "iot central app", command_type=central_monitor_ops, - ) as cmd_group: - cmd_group.command( - "monitor-events", - "monitor_events", - deprecate_info=self.deprecate( - redirect="iot central diagnostics monitor-events" - ), - ) diff --git a/azext_iot/central/models/template.py b/azext_iot/central/models/template.py index fe631ccd7..cfb767a84 100644 --- a/azext_iot/central/models/template.py +++ b/azext_iot/central/models/template.py @@ -15,30 +15,79 @@ def __init__(self, template: dict): 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, interface_name=""): - # interface_name has been specified, do a pointed lookup - if interface_name: - interface = self.interfaces.get(interface_name, {}) - return interface.get(name) + 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 interface - for interface in self.interfaces.values(): - schema = interface.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): + 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", {}) - interfaces = dcm.get("implements", {}) + + if dcm.get("contents"): + interfaces.append(self._extract_root_interface_contents(dcm)) + + if dcm.get("@type") == "CapabilityModel": + interfaces.extend(dcm.get("implements")) + else: + interfaces.extend(dcm.get("extends")) + return { - interface["name"]: self._extract_schemas(interface) + interface["@id"]: self._extract_schemas(interface) for interface in interfaces } except Exception: @@ -47,13 +96,13 @@ def _extract_interfaces(self, template: dict) -> dict: ) raise CLIError(details) - def _extract_schemas(self, interface: dict) -> dict: - return {schema["name"]: schema for schema in interface["schema"]["contents"]} + def _extract_schemas(self, entity: dict) -> dict: + return {schema["name"]: schema for schema in entity["schema"]["contents"]} - def _extract_schema_names(self, interfaces: dict) -> dict: + def _extract_schema_names(self, entity: dict) -> dict: return { - interface_name: list(interface_schemas.keys()) - for interface_name, interface_schemas in interfaces.items() + entity_name: list(entity_schemas.keys()) + for entity_name, entity_schemas in entity.items() } def _get_interface_list_property(self, property_name): diff --git a/azext_iot/central/params.py b/azext_iot/central/params.py index 98361d705..b800ccfa1 100644 --- a/azext_iot/central/params.py +++ b/azext_iot/central/params.py @@ -196,15 +196,3 @@ def load_central_arguments(self, _): context.argument( "module_id", options_list=["--module-id", "-m"], help="Provide IoT Edge Module ID if the device type is IoT Edge.", ) - # TODO: Delete this by end of Dec 2020 - load_deprecated_params(self, _) - - -def load_deprecated_params(self, _): - with self.argument_context("iot central app monitor-events") as context: - context.argument("timeout", arg_type=event_timeout_type) - context.argument("properties", arg_type=event_msg_prop_type) - context.argument( - "module_id", options_list=["--module-id", "-m"], help="Iot Edge Module ID", - ) - context.argument("minimum_severity", arg_type=severity_type) diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 5d80ead0e..4e30e223b 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -44,7 +44,7 @@ DEVICETWIN_MONITOR_TIME_SEC = 15 # (Lib name, minimum version (including), maximum version (excluding)) EVENT_LIB = ("uamqp", "1.2", "1.3") -CENTRAL_PNP_INTERFACE_PREFIX = "$iotin:" +PNP_DTDLV2_COMPONENT_MARKER = "__t" # Config Key's CONFIG_KEY_UAMQP_EXT_VERSION = "uamqp_ext_version" diff --git a/azext_iot/monitor/parsers/central_parser.py b/azext_iot/monitor/parsers/central_parser.py index 5e8001564..f5a8c4686 100644 --- a/azext_iot/monitor/parsers/central_parser.py +++ b/azext_iot/monitor/parsers/central_parser.py @@ -92,17 +92,29 @@ def _perform_dynamic_validations(self, payload: dict): if not isinstance(template, Template): return - # pnp device is sending data to an unrecognized interface - if self.interface_name and (self.interface_name not in template.interfaces): - details = strings.invalid_interface_name( - self.interface_name, list(template.interfaces.keys()) + # if component name is not defined then data should be mapped to root/inherited interfaces + if not self.component_name: + self._validate_payload( + payload=payload, template=template, is_component=False ) + return + + if not template.components: + # template does not have any valid components + details = strings.invalid_component_name(self.component_name, list()) self._add_central_issue(severity=Severity.warning, details=details) return - self._validate_payload_against_interfaces( - payload=payload, template=template, - ) + # if component name is defined check to see if its a valid name + if self.component_name not in template.components: + details = strings.invalid_component_name( + self.component_name, list(template.components.keys()) + ) + self._add_central_issue(severity=Severity.warning, details=details) + return + + # if component name is valid check to see if payload is valid + self._validate_payload(payload=payload, template=template, is_component=True) def _get_template(self): try: @@ -119,13 +131,13 @@ 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_against_interfaces( - self, payload: dict, template: Template, - ): + def _validate_payload(self, payload: dict, template: Template, is_component: bool): name_miss = [] for telemetry_name, telemetry in payload.items(): schema = template.get_schema( - name=telemetry_name, interface_name=self.interface_name + name=telemetry_name, + identifier=self.component_name, + is_component=is_component, ) if not schema: name_miss.append(telemetry_name) @@ -133,9 +145,14 @@ def _validate_payload_against_interfaces( self._process_telemetry(telemetry_name, schema, telemetry) if name_miss: - details = strings.invalid_field_name_mismatch_template( - name_miss, template.schema_names - ) + if is_component: + details = strings.invalid_field_name_component_mismatch_template( + name_miss, template.component_schema_names + ) + else: + details = strings.invalid_field_name_mismatch_template( + name_miss, template.schema_names, + ) self._add_central_issue(severity=Severity.warning, details=details) def _process_telemetry(self, telemetry_name: str, schema, telemetry): diff --git a/azext_iot/monitor/parsers/strings.py b/azext_iot/monitor/parsers/strings.py index 9c3450a1c..48123456b 100644 --- a/azext_iot/monitor/parsers/strings.py +++ b/azext_iot/monitor/parsers/strings.py @@ -56,10 +56,10 @@ def invalid_custom_headers(): # warning -def invalid_interface_name(interface_name: str, allowed_interfaces: list): +def invalid_component_name(component_name: str, allowed_components: list): return ( - "Device is specifying an interface that is unknown. Device specified interface: '{}'. Allowed interfaces: '{}'." - ).format(interface_name, allowed_interfaces) + "Device is specifying a component that is unknown. Device specified component: '{}'. Allowed components: '{}'." + ).format(component_name, allowed_components) # warning @@ -73,6 +73,17 @@ def invalid_field_name_mismatch_template( ).format(unmodeled_capabilities, modeled_capabilities) +# warning +def invalid_field_name_component_mismatch_template( + unmodeled_capabilities: list, modeled_capabilities: list +): + return ( + "Device is sending data that has not been defined in the device template. " + "Following capabilities have NOT been defined in the device template '{}'. " + "Following capabilities have been defined in the device template (grouped by components) '{}'. " + ).format(unmodeled_capabilities, modeled_capabilities) + + # warning def duplicate_property_name(duplicate_prop_name, interfaces: list): return ( diff --git a/azext_iot/monitor/property.py b/azext_iot/monitor/property.py index 3c78e3d88..b91a7e889 100644 --- a/azext_iot/monitor/property.py +++ b/azext_iot/monitor/property.py @@ -13,7 +13,7 @@ CENTRAL_ENDPOINT, DEVICETWIN_POLLING_INTERVAL_SEC, DEVICETWIN_MONITOR_TIME_SEC, - CENTRAL_PNP_INTERFACE_PREFIX, + PNP_DTDLV2_COMPONENT_MARKER, ) from azext_iot.central.models.devicetwin import DeviceTwin, Property @@ -77,10 +77,12 @@ def _is_relevant(self, key, val): return last_updated.timestamp() >= updated_within.timestamp() def _changed_props(self, prop, metadata, property_name): + # not an interface - whole thing is change log - if not self._is_interface(property_name): + if not self._is_component(prop): return prop - # iterate over property in the interface + + # iterate over properties in the component # if the property is not an exact match for what is present in the previous set of properties # track it as a change diff = { @@ -90,34 +92,29 @@ def _changed_props(self, prop, metadata, property_name): } return diff + def _is_component(self, prop): + return type(prop) == dict and prop.get(PNP_DTDLV2_COMPONENT_MARKER) == "c" + def _validate_payload(self, changes, minimum_severity): for value in changes: - issues = self._validate_payload_against_interfaces( + issues = self._validate_payload_against_entities( changes[value], value, minimum_severity ) for issue in issues: issue.log() - def _validate_payload_against_interfaces( - self, payload: dict, name, minimum_severity - ): + def _validate_payload_against_entities(self, payload: dict, name, minimum_severity): name_miss = [] issues_handler = IssueHandler() - interface_name = name.replace(CENTRAL_PNP_INTERFACE_PREFIX, "") - if self._is_interface(interface_name): - # if the payload is an interface then iterate thru the properties under the interface - for property_name in payload: - schema = self._template.get_schema( - name=property_name, interface_name=interface_name - ) - if not schema: - name_miss.append(property_name) - else: - # if the payload is a property then process the payload as a single unit. - schema = self._template.get_schema(name=name) + if not self._is_component(payload): + # update is not part of a component check under interfaces + schema = self._template.get_schema(name=name) if not schema: name_miss.append(name) + details = strings.invalid_field_name_mismatch_template( + name_miss, self._template.schema_names + ) interfaces_with_specified_property = self._template._get_interface_list_property( name @@ -134,11 +131,24 @@ def _validate_payload_against_interfaces( device_id=self._device_id, template_id=self._template.id, ) + else: + # Property update is part of a component perform additional validations under component list. + component_property_updates = [ + property_name + for property_name in payload + if property_name != PNP_DTDLV2_COMPONENT_MARKER + ] + for property_name in component_property_updates: + schema = self._template.get_schema( + name=property_name, identifier=name, is_component=True + ) + if not schema: + name_miss.append(property_name) + details = strings.invalid_field_name_component_mismatch_template( + name_miss, self._template.component_schema_names + ) if name_miss: - details = strings.invalid_field_name_mismatch_template( - name_miss, self._template.schema_names - ) issues_handler.add_central_issue( severity=Severity.warning, details=details, @@ -149,13 +159,6 @@ def _validate_payload_against_interfaces( return issues_handler.get_issues_with_minimum_severity(minimum_severity) - def _is_interface(self, interface_name): - # Remove PNP interface prefix to get the actual interface name - interface_name_modified = interface_name.replace( - CENTRAL_PNP_INTERFACE_PREFIX, "" - ) - return interface_name_modified in self._template.interfaces - def _get_device_template(self): device = self._central_device_provider.get_device(self._device_id) template = self._central_template_provider.get_device_template( diff --git a/azext_iot/tests/central/json/deeply_nested_template.json b/azext_iot/tests/central/json/deeply_nested_template.json index de66b86a4266646f0e390922d025050aeb0b8a5b..7231d657dac0eb680581e67046cae53b6e22b486 100644 GIT binary patch delta 18 acmaDeiRsKFrVR; Date: Wed, 16 Dec 2020 17:30:39 -0800 Subject: [PATCH 161/179] Use Az CLI released instead of edge. --- .azure-devops/templates/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-devops/templates/run-tests.yml b/.azure-devops/templates/run-tests.yml index 6fa49454d..3676cf7a6 100644 --- a/.azure-devops/templates/run-tests.yml +++ b/.azure-devops/templates/run-tests.yml @@ -1,7 +1,7 @@ parameters: pythonVersion: '' runUnitTestsOnly: 'true' - runWithAzureCliReleased: 'false' + runWithAzureCliReleased: 'true' steps: - task: UsePythonVersion@0 From 02c35511f0d86f28bfa5ad81091d68f87d0a7e68 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Mon, 4 Jan 2021 18:36:30 -0800 Subject: [PATCH 162/179] Release pipeline and Integration test refactor (#291) * Release pipeline and Integration test refactor * Moved integration tests into more structured folders and updated imports * Updated release pipeline to run individually grouped RP tests in concurrent tasks * Added correct unit test params to merge.yml * Path updates for credscan suppression * Moved dev test environment setup into template * Moved extension install and version detection into template * Modified parameters / var / and adjusted pipelines to use new values and templates --- .azure-devops/create-release.yml | 251 ++++++++++-------- .azure-devops/merge.yml | 6 + .../templates/install-and-record-version.yml | 15 ++ .azure-devops/templates/run-tests.yml | 61 ++--- .../templates/setup-dev-test-env.yml | 24 ++ CredScanSuppressions.json | 6 +- .../{ => central}/test_iot_central_int.py | 8 +- .../{ => central}/test_iot_central_unit.py | 4 +- .../test_iot_central_validator_unit.py | 4 +- azext_iot/tests/digitaltwins/__init__.py | 4 +- .../digitaltwins/test_dt_provider_unit.py | 2 +- .../test_dt_resource_lifecycle_int.py | 4 +- .../test_dt_twin_lifecycle_int.py | 2 +- azext_iot/tests/{ => dps}/test_iot_dps_int.py | 2 +- .../tests/{ => dps}/test_iot_dps_unit.py | 0 .../configurations/test_iot_config_int.py | 6 +- .../configurations/test_iot_config_unit.py | 2 +- .../tests/iothub/jobs/test_iothub_jobs_int.py | 4 +- .../iothub/jobs/test_iothub_jobs_unit.py | 2 +- .../pnp_runtime/test_iot_pnp_runtime_unit.py | 4 +- .../{ => iothub}/test_generic_replace.json | 0 .../tests/{ => iothub}/test_generic_twin.json | 0 .../tests/{ => iothub}/test_iot_ext_int.py | 4 +- .../tests/{ => iothub}/test_iot_ext_unit.py | 7 +- .../{ => iothub}/test_iot_messaging_int.py | 4 +- .../tests/iothub/test_iothub_discovery_int.py | 2 +- .../test_device_digitaltwin_interfaces.json | 58 ---- ...est_device_digitaltwin_invoke_command.json | 5 - ...st_device_digitaltwin_property_update.json | 11 - .../{ => utility}/test_iot_utility_unit.py | 4 +- .../test_monitor_parsers_unit.py | 4 +- .../tests/{ => utility}/test_uamqp_import.py | 0 32 files changed, 254 insertions(+), 256 deletions(-) create mode 100644 .azure-devops/templates/install-and-record-version.yml create mode 100644 .azure-devops/templates/setup-dev-test-env.yml rename azext_iot/tests/{ => central}/test_iot_central_int.py (98%) rename azext_iot/tests/{ => central}/test_iot_central_unit.py (99%) rename azext_iot/tests/{ => central}/test_iot_central_validator_unit.py (99%) rename azext_iot/tests/{ => dps}/test_iot_dps_int.py (99%) rename azext_iot/tests/{ => dps}/test_iot_dps_unit.py (100%) rename azext_iot/tests/{ => iothub}/test_generic_replace.json (100%) rename azext_iot/tests/{ => iothub}/test_generic_twin.json (100%) rename azext_iot/tests/{ => iothub}/test_iot_ext_int.py (99%) rename azext_iot/tests/{ => iothub}/test_iot_ext_unit.py (99%) rename azext_iot/tests/{ => iothub}/test_iot_messaging_int.py (99%) delete mode 100644 azext_iot/tests/test_device_digitaltwin_interfaces.json delete mode 100644 azext_iot/tests/test_device_digitaltwin_invoke_command.json delete mode 100644 azext_iot/tests/test_device_digitaltwin_property_update.json rename azext_iot/tests/{ => utility}/test_iot_utility_unit.py (98%) rename azext_iot/tests/{ => utility}/test_monitor_parsers_unit.py (99%) rename azext_iot/tests/{ => utility}/test_uamqp_import.py (100%) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index be446801a..f69eb669c 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -2,109 +2,148 @@ pr: none trigger: none -jobs: - - # track deployment in environments -- deployment: 'StageGitHub' - displayName: 'Stage CLI extension on GitHub' - environment: 'production' - -- job: 'Build_Publish_Azure_IoT_CLI_Extension' - pool: - vmImage: 'Ubuntu-16.04' - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.6.x' - architecture: 'x64' - - - 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-16.04' - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.6.x' - architecture: 'x64' - - - template: templates/setup-ci-machine.yml - - - template: templates/build-publish-azure-cli-test-sdk.yml - -- job: 'Run_Tests' - dependsOn : Build_Publish_Azure_CLI_Test_SDK - pool: - vmImage: 'Ubuntu-16.04' - - steps: - - template: templates/run-tests.yml - parameters: - pythonVersion: '3.6.x' - runUnitTestsOnly: 'true' - - - script: 'pip install .' - displayName: 'Install the whl locally' - workingDirectory: '.' - - - task: PythonScript@0 - displayName : 'setupVersion' - name: 'setupVersion' - inputs: - scriptSource: 'inline' - script: | - from azext_iot.constants import VERSION - print("Version is " + VERSION) - print("##vso[task.setvariable variable=CLIVersion;isOutput=true]"+VERSION) - print("##vso[task.setvariable variable=ReleaseTitle;isOutput=true]"+"azure-iot "+VERSION) - -- job: 'Calculate_Sha_And_Create_Release' - dependsOn : Run_Tests - pool: - vmImage: 'vs2017-win2016' - variables: - CLIVersion: $[dependencies.Run_Tests.outputs['setupVersion.CLIVersion']] - ReleaseTitle: $[dependencies.Run_Tests.outputs['setupVersion.ReleaseTitle']] - - steps: - - task: DownloadBuildArtifacts@0 - displayName : 'Download Extension wheel from Build Artifacts' - inputs: - buildType: 'current' - downloadType: 'single' - artifactName: 'azure-iot' - downloadPath: '$(System.ArtifactsDirectory)/extension' - - - task: PowerShell@2 - displayName: 'Calculate sha for downloaded extension' - inputs: - targetType: 'inline' - script: | - $extensions = Get-ChildItem -Filter "*.whl" -Recurse | Select-Object FullName - Foreach ($extension in $extensions) - { - Write-Host "calculating sha256 for " $extension.FullName - (Get-Filehash -Path $extension.Fullname -Algorithm SHA256).Hash.ToLower() - } - Write-Host "done" - workingDirectory: '$(System.ArtifactsDirectory)/extension' - - - task: GitHubRelease@0 - inputs: - gitHubConnection: AzIoTCLIGitHub - repositoryName: $(Build.Repository.Name) - action: 'create' - target: '$(Build.SourceVersion)' - tagSource: manual - tag: 'v$(CLIVersion)' - title: $(ReleaseTitle) - assets: '$(System.ArtifactsDirectory)/extension/**/*.whl' - assetUploadMode: 'replace' - isDraft: true - isPreRelease: false - addChangeLog: false +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-16.04' + + 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-16.04' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: $(pythonVersion) + architecture: $(architecture) + + - template: templates/setup-ci-machine.yml + + - template: templates/build-publish-azure-cli-test-sdk.yml + + - stage: 'test' + displayName: 'Run tests' + pool: + vmImage: 'Ubuntu-16.04' + dependsOn: build + jobs: + - job: 'testCentral' + displayName: 'Test IoT Central' + steps: + - template: templates/run-tests.yml + parameters: + path: 'azext_iot/tests/central' + name: 'IoT-Central' + + - job: 'testADT' + displayName: 'Test Azure DigitalTwins' + steps: + - template: templates/run-tests.yml + parameters: + path: 'azext_iot/tests/digitaltwins' + name: 'Azure-DigitalTwins' + + - job: 'testDPS' + displayName: 'Test DPS' + steps: + - template: templates/run-tests.yml + parameters: + path: 'azext_iot/tests/dps' + name: 'Device-Provisioning-Service' + + - job: 'testHub' + displayName: 'Test IoT Hub' + steps: + - template: templates/run-tests.yml + parameters: + path: 'azext_iot/tests/iothub' + name: 'IoT-Hub' + + - job: 'unitTests' + displayName: 'Unit tests and code coverage' + steps: + - template: templates/run-tests.yml + parameters: + runIntTests: 'false' + runUnitTests: 'true' + + - job: 'recordVersion' + displayName: 'Install and verify version' + steps: + - template: templates/setup-dev-test-env.yml + parameters: + pythonVersion: $(pythonVersion) + architecture: $(architecture) + + - template: templates/install-and-record-version.yml + + - stage: 'release' + displayName: 'Stage GitHub release' + dependsOn: [build, test] + jobs: + - deployment: 'StageGitHub' + displayName: 'Stage CLI extension on GitHub' + environment: 'production' + + - job: 'Calculate_Sha_And_Create_Release' + pool: + vmImage: 'vs2017-win2016' + variables: + CLIVersion: $[ stageDependencies.test.recordVersion.outputs['setupVersion.CLIVersion'] ] + ReleaseTitle: $[ stageDependencies.test.recordVersion.outputs['setupVersion.ReleaseTitle'] ] + + steps: + - task: DownloadBuildArtifacts@0 + displayName : 'Download Extension wheel from Build Artifacts' + inputs: + buildType: 'current' + downloadType: 'single' + artifactName: 'azure-iot' + downloadPath: '$(System.ArtifactsDirectory)/extension' + + - task: PowerShell@2 + displayName: 'Calculate sha for downloaded extension' + inputs: + targetType: 'inline' + script: | + $extensions = Get-ChildItem -Filter "*.whl" -Recurse | Select-Object FullName + Foreach ($extension in $extensions) + { + Write-Host "calculating sha256 for " $extension.FullName + (Get-Filehash -Path $extension.Fullname -Algorithm SHA256).Hash.ToLower() + } + Write-Host "done" + workingDirectory: '$(System.ArtifactsDirectory)/extension' + + - task: GitHubRelease@0 + inputs: + gitHubConnection: AzIoTCLIGitHub + repositoryName: $(Build.Repository.Name) + action: 'create' + target: '$(Build.SourceVersion)' + tagSource: manual + tag: 'v$(CLIVersion)' + title: $(ReleaseTitle) + assets: '$(System.ArtifactsDirectory)/extension/**/*.whl' + assetUploadMode: 'replace' + isDraft: true + isPreRelease: false + addChangeLog: false diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 1dab1cdc0..388c27962 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -58,6 +58,8 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '$(python.version)' + runUnitTests: 'true' + runIntTests: 'false' - job: 'run_unit_tests_macOs' dependsOn: ['build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] @@ -68,6 +70,8 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.8.x' + runUnitTests: 'true' + runIntTests: 'false' - job: 'run_unit_tests_windows' dependsOn : [ 'build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] @@ -83,6 +87,8 @@ jobs: - template: templates/run-tests.yml parameters: pythonVersion: '3.8.x' + runUnitTests: 'true' + runIntTests: 'false' - job: 'run_style_check' dependsOn: ['build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] diff --git a/.azure-devops/templates/install-and-record-version.yml b/.azure-devops/templates/install-and-record-version.yml new file mode 100644 index 000000000..7c26af21b --- /dev/null +++ b/.azure-devops/templates/install-and-record-version.yml @@ -0,0 +1,15 @@ +steps: +- script: 'pip install .' + displayName: 'Install the whl locally' + workingDirectory: '.' + +- task: PythonScript@0 + displayName : 'setupVersion' + name: 'setupVersion' + inputs: + scriptSource: 'inline' + script: | + from azext_iot.constants import VERSION + print("Version is " + VERSION) + print("##vso[task.setvariable variable=CLIVersion;isOutput=true]"+VERSION) + print("##vso[task.setvariable variable=ReleaseTitle;isOutput=true]"+"azure-iot "+VERSION) diff --git a/.azure-devops/templates/run-tests.yml b/.azure-devops/templates/run-tests.yml index 3676cf7a6..3a86da36f 100644 --- a/.azure-devops/templates/run-tests.yml +++ b/.azure-devops/templates/run-tests.yml @@ -1,51 +1,40 @@ parameters: - pythonVersion: '' - runUnitTestsOnly: 'true' + pythonVersion: '3.6.x' + architecture: 'x64' + runUnitTests: 'false' + runIntTests: 'true' runWithAzureCliReleased: 'true' + path: 'azext_iot/tests' + name: 'All' steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: ${{ parameters.pythonVersion }} - architecture: 'x64' - - - ${{ if eq(parameters.runWithAzureCliReleased, 'false') }}: - - template: install-azure-cli-edge.yml - - - ${{ if eq(parameters.runWithAzureCliReleased, 'true') }}: - - template: install-azure-cli-released.yml - - - template: download-install-local-azure-test-sdk.yml - - - template: setup-ci-machine.yml - - - template: download-install-local-azure-iot-cli-extension.yml - - - template: set-pythonpath.yml + - template: setup-dev-test-env.yml + parameters: + architecture: ${{ parameters.architecture }} + pythonVersion: ${{ parameters.pythonVersion }} + runWithAzureCliReleased: ${{ parameters.runWithAzureCliReleased }} - template: set-testenv-sentinel.yml - - ${{ if eq(parameters.runUnitTestsOnly, 'false') }}: - + - ${{ if eq(parameters.runIntTests, 'true') }}: - task: AzureCLI@2 - displayName: 'Execute full IoT extension test run' + displayName: '${{ parameters.name }} integration tests' inputs: azureSubscription: AzIoTCLIService scriptType: bash scriptLocation: inlineScript - inlineScript: pytest -vv -r azext_iot/tests/ --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-full-results.xml --cov-report=xml - - - ${{ if eq(parameters.runUnitTestsOnly, 'true') }}: + inlineScript: pytest -vv ${{ parameters.path }} -k "_int" --junitxml=junit/test-iotext-int-${{ parameters.name }}.xml - - script: pytest -vv -r azext_iot/tests/ -k "_unit" --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-unit-results.xml --cov-report=xml - displayName: 'Execute IoT extension unit tests' - - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: 'cobertura' - summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' - reportDirectory: '$(System.DefaultWorkingDirectory)/htmlcov' - additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/htmlcov/**/*.*' + - ${{ 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 --cov-report=xml + displayName: '${{ parameters.name }} unit tests' + + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: 'cobertura' + summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' + reportDirectory: '$(System.DefaultWorkingDirectory)/htmlcov' + additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/htmlcov/**/*.*' - task: PublishTestResults@2 condition: succeededOrFailed() @@ -53,5 +42,5 @@ steps: inputs: testResultsFormat: 'JUnit' testResultsFiles: '**/test-*.xml' - testRunTitle: 'Publish test results for Python ${{ parameters.pythonVersion }} on OS $(Agent.OS)' + testRunTitle: 'Publish ${{ parameters.name }} test results for Python ${{ parameters.pythonVersion }} on OS $(Agent.OS)' searchFolder: '$(System.DefaultWorkingDirectory)' diff --git a/.azure-devops/templates/setup-dev-test-env.yml b/.azure-devops/templates/setup-dev-test-env.yml new file mode 100644 index 000000000..cc6c1a875 --- /dev/null +++ b/.azure-devops/templates/setup-dev-test-env.yml @@ -0,0 +1,24 @@ +parameters: + pythonVersion: '' + architecture: '' + runWithAzureCliReleased: 'true' + +steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: ${{ parameters.pythonVersion }} + architecture: ${{ parameters.architecture }} + + - ${{ if eq(parameters.runWithAzureCliReleased, 'false') }}: + - template: install-azure-cli-edge.yml + + - ${{ if eq(parameters.runWithAzureCliReleased, 'true') }}: + - template: install-azure-cli-released.yml + + - template: download-install-local-azure-test-sdk.yml + + - template: setup-ci-machine.yml + + - template: download-install-local-azure-iot-cli-extension.yml + + - template: set-pythonpath.yml \ No newline at end of file diff --git a/CredScanSuppressions.json b/CredScanSuppressions.json index 3f349f26e..20e6d7c60 100644 --- a/CredScanSuppressions.json +++ b/CredScanSuppressions.json @@ -20,16 +20,16 @@ "_justification": "Completely made up keys for unit tests." }, { - "file": "azext_iot\\tests\\test_iot_dps_int.py", + "file": "azext_iot\\tests\\dps\\test_iot_dps_int.py", "placeholder": "cT/EXZvsplPEpT//p98Pc6sKh8mY3kYgSxavHwMkl7w=", "_justification": "Ensure made up endorsement key evaluates to the expected device key." }, { - "file": "azext_iot\\tests\\test_iot_dps_unit.py", + "file": "azext_iot\\tests\\dps\\test_iot_dps_unit.py", "_justification": "Completely made up keys for unit tests." }, { - "file": "azext_iot\\tests\\test_iot_ext_unit.py", + "file": "azext_iot\\tests\\iothub\\test_iot_ext_unit.py", "placeholder": "+XLy+MVZ+aTeOnVzN2kLeB16O+kSxmz6g3rS6fAf6rw=", "_justification": "Ensure made up policy key generates the proper SAS token." } diff --git a/azext_iot/tests/test_iot_central_int.py b/azext_iot/tests/central/test_iot_central_int.py similarity index 98% rename from azext_iot/tests/test_iot_central_int.py rename to azext_iot/tests/central/test_iot_central_int.py index 4d80e9f0a..fba2f3fe5 100644 --- a/azext_iot/tests/test_iot_central_int.py +++ b/azext_iot/tests/central/test_iot_central_int.py @@ -10,22 +10,22 @@ import time import pytest -from .conftest import get_context_path +from azext_iot.tests.conftest import get_context_path from azure.iot.device import Message from azext_iot.common import utility from azext_iot.central.models.enum import DeviceStatus, Role from azext_iot.monitor.parsers import strings -from . import CaptureOutputLiveScenarioTest, helpers +from azext_iot.tests import CaptureOutputLiveScenarioTest, helpers APP_ID = os.environ.get("azext_iot_central_app_id") APP_PRIMARY_KEY = os.environ.get("azext_iot_central_primarykey") APP_SCOPE_ID = os.environ.get("azext_iot_central_scope_id") device_template_path = get_context_path( - __file__, "central/json/device_template_int_test.json" + __file__, "json/device_template_int_test.json" ) -sync_command_params = get_context_path(__file__, "central/json/sync_command_args.json") +sync_command_params = get_context_path(__file__, "json/sync_command_args.json") if not all([APP_ID]): raise ValueError("Set azext_iot_central_app_id to run central integration tests.") diff --git a/azext_iot/tests/test_iot_central_unit.py b/azext_iot/tests/central/test_iot_central_unit.py similarity index 99% rename from azext_iot/tests/test_iot_central_unit.py rename to azext_iot/tests/central/test_iot_central_unit.py index 50b1d1933..cd09b11bd 100644 --- a/azext_iot/tests/test_iot_central_unit.py +++ b/azext_iot/tests/central/test_iot_central_unit.py @@ -22,8 +22,8 @@ from azext_iot.central.models.template import Template from azext_iot.monitor.property import PropertyMonitor from azext_iot.monitor.models.enum import Severity -from .helpers import load_json -from .test_constants import FileNames +from azext_iot.tests.helpers import load_json +from azext_iot.tests.test_constants import FileNames from azext_iot.constants import PNP_DTDLV2_COMPONENT_MARKER device_id = "mydevice" diff --git a/azext_iot/tests/test_iot_central_validator_unit.py b/azext_iot/tests/central/test_iot_central_validator_unit.py similarity index 99% rename from azext_iot/tests/test_iot_central_validator_unit.py rename to azext_iot/tests/central/test_iot_central_validator_unit.py index 844b5c6ac..75bafe4bc 100644 --- a/azext_iot/tests/test_iot_central_validator_unit.py +++ b/azext_iot/tests/central/test_iot_central_validator_unit.py @@ -10,8 +10,8 @@ from azext_iot.central.models.template import Template from azext_iot.monitor.central_validator import validate, extract_schema_type -from .helpers import load_json -from .test_constants import FileNames +from azext_iot.tests.helpers import load_json +from azext_iot.tests.test_constants import FileNames class TestTemplateValidations: diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py index 5c97dff96..c90956dda 100644 --- a/azext_iot/tests/digitaltwins/__init__.py +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -6,8 +6,8 @@ import pytest import os -from ..generators import generate_generic_id -from ..settings import DynamoSettings +from azext_iot.tests.generators import generate_generic_id +from azext_iot.tests.settings import DynamoSettings from azure.cli.testsdk import LiveScenarioTest from azext_iot.common.embedded_cli import EmbeddedCLI diff --git a/azext_iot/tests/digitaltwins/test_dt_provider_unit.py b/azext_iot/tests/digitaltwins/test_dt_provider_unit.py index e6d7eb3b8..58a9e5ffd 100644 --- a/azext_iot/tests/digitaltwins/test_dt_provider_unit.py +++ b/azext_iot/tests/digitaltwins/test_dt_provider_unit.py @@ -9,7 +9,7 @@ import responses import json from azext_iot.digitaltwins.providers.base import DigitalTwinsProvider -from ..generators import generate_generic_id +from azext_iot.tests.generators import generate_generic_id resource_group = generate_generic_id() 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 02cd2da5c..b2f682896 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -8,7 +8,7 @@ from time import sleep from knack.log import get_logger from azext_iot.digitaltwins.common import ADTEndpointType -from ..settings import DynamoSettings +from azext_iot.tests.settings import DynamoSettings from . import DTLiveScenarioTest from . import ( MOCK_RESOURCE_TAGS, @@ -246,7 +246,7 @@ def test_dt_endpoints_routes(self): ) ) - sleep(20) # Wait for service to catch-up + sleep(60) # Wait for service to catch-up list_ep_output = self.cmd( "dt endpoint list -n {}".format(endpoints_instance_name) 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 5c48e5979..33b7ab2ea 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -42,7 +42,7 @@ def test_dt_twin(self): ) ) # Wait for RBAC to catch-up - sleep(20) + sleep(60) self.cmd( "dt model create -n {} --from-directory '{}'".format( diff --git a/azext_iot/tests/test_iot_dps_int.py b/azext_iot/tests/dps/test_iot_dps_int.py similarity index 99% rename from azext_iot/tests/test_iot_dps_int.py rename to azext_iot/tests/dps/test_iot_dps_int.py index ba78fa09d..b9ca59899 100644 --- a/azext_iot/tests/test_iot_dps_int.py +++ b/azext_iot/tests/dps/test_iot_dps_int.py @@ -11,7 +11,7 @@ from azext_iot.common import embedded_cli from azext_iot.common.utility import generate_key from azext_iot.iothub.providers.discovery import IotHubDiscovery -from .settings import Setting +from azext_iot.tests.settings import Setting # Set these to the proper IoT Hub DPS, IoT Hub and Resource Group for Integration Tests. dps = os.environ.get("azext_iot_testdps") diff --git a/azext_iot/tests/test_iot_dps_unit.py b/azext_iot/tests/dps/test_iot_dps_unit.py similarity index 100% rename from azext_iot/tests/test_iot_dps_unit.py rename to azext_iot/tests/dps/test_iot_dps_unit.py 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 8b6c00429..8c4997e96 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_int.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_int.py @@ -7,9 +7,9 @@ import random import json -from ... import IoTLiveScenarioTest -from ...conftest import get_context_path -from ...settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.tests.conftest import get_context_path +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC from azext_iot.common.utility import read_file_content settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) diff --git a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py index 1edcac871..cb54909c2 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py @@ -14,7 +14,7 @@ from knack.cli import CLIError from azext_iot.operations import hub as subject from azext_iot.common.utility import read_file_content, evaluate_literal -from ...conftest import build_mock_response, path_service_client, mock_target, get_context_path +from azext_iot.tests.conftest import build_mock_response, path_service_client, mock_target, get_context_path config_id = "myconfig-{}".format(str(uuid4()).replace("-", "")) 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 d9c47df0b..1bd76bbcc 100644 --- a/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py +++ b/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py @@ -6,8 +6,8 @@ import json from datetime import datetime, timedelta -from ... import IoTLiveScenarioTest -from ...settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.tests import IoTLiveScenarioTest +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) diff --git a/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py b/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py index 2d4aff133..491e5ec26 100644 --- a/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py +++ b/azext_iot/tests/iothub/jobs/test_iothub_jobs_unit.py @@ -13,7 +13,7 @@ from knack.cli import CLIError from azext_iot.iothub import commands_job as subject from azext_iot.common.shared import JobStatusType, JobType -from ...conftest import build_mock_response, path_service_client, mock_target +from azext_iot.tests.conftest import build_mock_response, path_service_client, mock_target def generate_job_id(): diff --git a/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py b/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py index e36fa1f96..8011e48f5 100644 --- a/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py +++ b/azext_iot/tests/iothub/pnp_runtime/test_iot_pnp_runtime_unit.py @@ -11,8 +11,8 @@ from random import randint from knack.cli import CLIError from azext_iot.iothub import commands_pnp_runtime as subject -from ...conftest import mock_target -from ...generators import generate_generic_id +from azext_iot.tests.conftest import mock_target +from azext_iot.tests.generators import generate_generic_id device_id = generate_generic_id() diff --git a/azext_iot/tests/test_generic_replace.json b/azext_iot/tests/iothub/test_generic_replace.json similarity index 100% rename from azext_iot/tests/test_generic_replace.json rename to azext_iot/tests/iothub/test_generic_replace.json diff --git a/azext_iot/tests/test_generic_twin.json b/azext_iot/tests/iothub/test_generic_twin.json similarity index 100% rename from azext_iot/tests/test_generic_twin.json rename to azext_iot/tests/iothub/test_generic_twin.json diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/iothub/test_iot_ext_int.py similarity index 99% rename from azext_iot/tests/test_iot_ext_int.py rename to azext_iot/tests/iothub/test_iot_ext_int.py index 925ab99d0..0114e3c37 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/iothub/test_iot_ext_int.py @@ -9,8 +9,8 @@ import warnings from azext_iot.common.utility import read_file_content -from . import IoTLiveScenarioTest -from .settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +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 opt_env_set = ["azext_iot_teststorageuri", "azext_iot_identity_teststorageid"] diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/iothub/test_iot_ext_unit.py similarity index 99% rename from azext_iot/tests/test_iot_ext_unit.py rename to azext_iot/tests/iothub/test_iot_ext_unit.py index 4e79a2234..f28bd86db 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/iothub/test_iot_ext_unit.py @@ -28,7 +28,7 @@ from azext_iot.constants import TRACING_PROPERTY, USER_AGENT, BASE_MQTT_API_VERSION from azext_iot.tests.generators import create_req_monitor_events, generate_generic_id from knack.util import CLIError -from .conftest import ( +from azext_iot.tests.conftest import ( fixture_cmd, build_mock_response, path_service_client, @@ -1682,7 +1682,7 @@ def test_device_method_error(self, serviceclient_generic_error): class TestCloudToDeviceMessaging: @pytest.fixture(params=["full", "min"]) def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): - from .generators import create_c2d_receive_response + from azext_iot.tests.generators import create_c2d_receive_response if request.param == "full": payload = create_c2d_receive_response() @@ -1706,8 +1706,7 @@ def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): @pytest.fixture() def c2d_receive_ack_scenario(self, fixture_ghcs, mocked_response): - from .generators import create_c2d_receive_response - + from azext_iot.tests.generators import create_c2d_receive_response payload = create_c2d_receive_response() mocked_response.add( method=responses.GET, diff --git a/azext_iot/tests/test_iot_messaging_int.py b/azext_iot/tests/iothub/test_iot_messaging_int.py similarity index 99% rename from azext_iot/tests/test_iot_messaging_int.py rename to azext_iot/tests/iothub/test_iot_messaging_int.py index 286948d95..6b767631d 100644 --- a/azext_iot/tests/test_iot_messaging_int.py +++ b/azext_iot/tests/iothub/test_iot_messaging_int.py @@ -9,8 +9,8 @@ from time import time from uuid import uuid4 -from . import IoTLiveScenarioTest, PREFIX_DEVICE -from .settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC +from azext_iot.tests import IoTLiveScenarioTest, PREFIX_DEVICE +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC from azext_iot.common.utility import ( validate_min_python_version, execute_onthread, diff --git a/azext_iot/tests/iothub/test_iothub_discovery_int.py b/azext_iot/tests/iothub/test_iothub_discovery_int.py index 16927cd51..b0bd35821 100644 --- a/azext_iot/tests/iothub/test_iothub_discovery_int.py +++ b/azext_iot/tests/iothub/test_iothub_discovery_int.py @@ -9,7 +9,7 @@ PRIVILEDGED_ACCESS_RIGHTS_SET, ) from azext_iot.tests import IoTLiveScenarioTest -from ..settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC, Setting +from azext_iot.tests.settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC, Setting settings = DynamoSettings(ENV_SET_TEST_IOTHUB_BASIC) LIVE_HUB = settings.env.azext_iot_testhub diff --git a/azext_iot/tests/test_device_digitaltwin_interfaces.json b/azext_iot/tests/test_device_digitaltwin_interfaces.json deleted file mode 100644 index 22cf08ae5..000000000 --- a/azext_iot/tests/test_device_digitaltwin_interfaces.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "interfaces": { - "environmentalSensor": { - "name": "environmentalSensor", - "properties": { - "brightness": { - "desired": { - "value": 123 - }, - "reported": { - "desiredState": { - "code": 200, - "description": "Brightness updated", - "version": 4 - }, - "value": 123 - } - }, - "name": { - "desired": { - "value": "test" - }, - "reported": { - "desiredState": { - "code": 200, - "description": "Property Updated Successfully", - "version": 4 - }, - "value": "test" - } - }, - "state": { - "reported": { - "value": true - } - } - } - }, - "urn_azureiot_ModelDiscovery_DigitalTwin": { - "name": "urn_azureiot_ModelDiscovery_DigitalTwin", - "properties": { - "modelInformation": { - "reported": { - "value": { - "interfaces": { - "environmentalSensor": "urn:contoso:com:EnvironmentalSensor:1", - "urn_azureiot_ModelDiscovery_DigitalTwin": "urn:azureiot:ModelDiscovery:DigitalTwin:1", - "urn_azureiot_ModelDiscovery_ModelInformation": "urn:azureiot:ModelDiscovery:ModelInformation:1" - }, - "modelId": "urn:azureiot:testdevicecapabilitymodel:1" - } - } - } - } - } - }, - "version": 1 - } \ No newline at end of file diff --git a/azext_iot/tests/test_device_digitaltwin_invoke_command.json b/azext_iot/tests/test_device_digitaltwin_invoke_command.json deleted file mode 100644 index fe34ec6b9..000000000 --- a/azext_iot/tests/test_device_digitaltwin_invoke_command.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "blinkRequest": { - "interval": 1 - } -} \ No newline at end of file diff --git a/azext_iot/tests/test_device_digitaltwin_property_update.json b/azext_iot/tests/test_device_digitaltwin_property_update.json deleted file mode 100644 index 09bcbd6b4..000000000 --- a/azext_iot/tests/test_device_digitaltwin_property_update.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "environmentalSensor": { - "properties": { - "name": { - "desired": { - "value": "test-update" - } - } - } - } -} \ No newline at end of file diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/utility/test_iot_utility_unit.py similarity index 98% rename from azext_iot/tests/test_iot_utility_unit.py rename to azext_iot/tests/utility/test_iot_utility_unit.py index 19f8b3441..62516539e 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/utility/test_iot_utility_unit.py @@ -242,7 +242,7 @@ def test_inline_json_fail(self, content, argname): @pytest.mark.parametrize( "content, argname", - [("iothub/configurations/test_adm_device_content.json", "myarg0")], + [("../iothub/configurations/test_adm_device_content.json", "myarg0")], ) def test_file_json(self, content, argname, set_cwd): result = process_json_arg(content, argument_name=argname) @@ -267,7 +267,7 @@ def test_file_json_fail_invalidpath(self, content, argname, set_cwd, mocker): ) assert mocked_util_logger.call_args[0][1] == argname - @pytest.mark.parametrize("content, argname", [("generators.py", "myarg0")]) + @pytest.mark.parametrize("content, argname", [("../generators.py", "myarg0")]) def test_file_json_fail_invalidcontent(self, content, argname, set_cwd, mocker): mocked_util_logger = mocker.patch.object(logger, "warning", autospec=True) diff --git a/azext_iot/tests/test_monitor_parsers_unit.py b/azext_iot/tests/utility/test_monitor_parsers_unit.py similarity index 99% rename from azext_iot/tests/test_monitor_parsers_unit.py rename to azext_iot/tests/utility/test_monitor_parsers_unit.py index d8c4f9a8f..3d1622181 100644 --- a/azext_iot/tests/test_monitor_parsers_unit.py +++ b/azext_iot/tests/utility/test_monitor_parsers_unit.py @@ -19,8 +19,8 @@ from azext_iot.monitor.parsers import strings from azext_iot.monitor.models.arguments import CommonParserArguments from azext_iot.monitor.models.enum import Severity -from .helpers import load_json -from .test_constants import FileNames +from azext_iot.tests.helpers import load_json +from azext_iot.tests.test_constants import FileNames def _encode_app_props(app_props: dict): diff --git a/azext_iot/tests/test_uamqp_import.py b/azext_iot/tests/utility/test_uamqp_import.py similarity index 100% rename from azext_iot/tests/test_uamqp_import.py rename to azext_iot/tests/utility/test_uamqp_import.py From c9c5afd0e94ad29782a923e8294bde069e0a4665 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 6 Jan 2021 13:49:43 -0800 Subject: [PATCH 163/179] Supports distributed code coverage metrics. (#292) --- .azure-devops/create-release.yml | 24 +++++++++---- .azure-devops/merge.yml | 2 ++ .../templates/calculate-code-coverage.yml | 34 +++++++++++++++++++ .azure-devops/templates/run-tests.yml | 24 +++++++------ 4 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 .azure-devops/templates/calculate-code-coverage.yml diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index f69eb669c..572588f78 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -51,7 +51,7 @@ stages: - template: templates/run-tests.yml parameters: path: 'azext_iot/tests/central' - name: 'IoT-Central' + name: 'iot-central' - job: 'testADT' displayName: 'Test Azure DigitalTwins' @@ -59,15 +59,15 @@ stages: - template: templates/run-tests.yml parameters: path: 'azext_iot/tests/digitaltwins' - name: 'Azure-DigitalTwins' - + name: 'azure-digitaltwins' + - job: 'testDPS' displayName: 'Test DPS' steps: - template: templates/run-tests.yml parameters: path: 'azext_iot/tests/dps' - name: 'Device-Provisioning-Service' + name: 'device-provisioning-service' - job: 'testHub' displayName: 'Test IoT Hub' @@ -75,8 +75,8 @@ stages: - template: templates/run-tests.yml parameters: path: 'azext_iot/tests/iothub' - name: 'IoT-Hub' - + name: 'iot-hub' + - job: 'unitTests' displayName: 'Unit tests and code coverage' steps: @@ -95,6 +95,18 @@ stages: - template: templates/install-and-record-version.yml + - 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) + - stage: 'release' displayName: 'Stage GitHub release' dependsOn: [build, test] diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 388c27962..762950505 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -73,6 +73,8 @@ jobs: runUnitTests: 'true' runIntTests: 'false' + - template: templates/calculate-code-coverage.yml + - job: 'run_unit_tests_windows' dependsOn : [ 'build_and_publish_azure_iot_cli_ext', 'build_and_publish_azure_cli_test_sdk'] pool: diff --git a/.azure-devops/templates/calculate-code-coverage.yml b/.azure-devops/templates/calculate-code-coverage.yml new file mode 100644 index 000000000..eb14c272a --- /dev/null +++ b/.azure-devops/templates/calculate-code-coverage.yml @@ -0,0 +1,34 @@ +parameters: + pythonVersion: '3.6.x' + architecture: 'x64' + +steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: ${{ parameters.pythonVersion }} + architecture: ${{ parameters.architecture }} + + - template: setup-ci-machine.yml + + - task: DownloadBuildArtifacts@0 + displayName : 'Download code coverage KPIs' + inputs: + buildType: 'current' + downloadType: 'single' + artifactName: 'coverage' + downloadPath: '$(System.ArtifactsDirectory)/aziotext_kpi/' + + - bash: | + ls -l ./coverage + export COVERAGE_FILE=.coverage.combined + for i in ./coverage/.coverage.*; do + coverage combine -a $i + done + coverage xml --rcfile="$(System.DefaultWorkingDirectory)/.coveragerc" + workingDirectory: '$(System.ArtifactsDirectory)/aziotext_kpi/' + displayName: 'Merging code coverage data' + + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: 'cobertura' + summaryFileLocation: '$(System.ArtifactsDirectory)/aziotext_kpi/coverage.xml' diff --git a/.azure-devops/templates/run-tests.yml b/.azure-devops/templates/run-tests.yml index 3a86da36f..58840281d 100644 --- a/.azure-devops/templates/run-tests.yml +++ b/.azure-devops/templates/run-tests.yml @@ -5,7 +5,7 @@ parameters: runIntTests: 'true' runWithAzureCliReleased: 'true' path: 'azext_iot/tests' - name: 'All' + name: 'all' steps: - template: setup-dev-test-env.yml @@ -23,18 +23,22 @@ steps: azureSubscription: AzIoTCLIService scriptType: bash scriptLocation: inlineScript - inlineScript: pytest -vv ${{ parameters.path }} -k "_int" --junitxml=junit/test-iotext-int-${{ parameters.name }}.xml + inlineScript: | + 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 --cov-report=xml + - 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' - - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: 'cobertura' - summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' - reportDirectory: '$(System.DefaultWorkingDirectory)/htmlcov' - additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/htmlcov/**/*.*' + env: + COVERAGE_FILE: .coverage.${{ parameters.name }} + + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: .coverage.${{ parameters.name }} + publishLocation: 'Container' + artifactName: 'coverage' - task: PublishTestResults@2 condition: succeededOrFailed() From e7fbe71f9859dd22e6c9376212f3de9164e37bfb Mon Sep 17 00:00:00 2001 From: Ryan K Date: Thu, 21 Jan 2021 17:39:52 -0800 Subject: [PATCH 164/179] Added UT/IT for enrollment and enrollment-group with array in initial props (#298) --- azext_iot/tests/dps/test_iot_dps_int.py | 99 +++++++++++++++++++++++- azext_iot/tests/dps/test_iot_dps_unit.py | 17 ++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/azext_iot/tests/dps/test_iot_dps_int.py b/azext_iot/tests/dps/test_iot_dps_int.py index b9ca59899..4cff8cd72 100644 --- a/azext_iot/tests/dps/test_iot_dps_int.py +++ b/azext_iot/tests/dps/test_iot_dps_int.py @@ -105,12 +105,19 @@ def __init__(self, test_method): output_dir = os.getcwd() create_self_signed_certificate(cert_name, 200, output_dir, True) - self.kwargs["generic_dict"] = { + base_enrollment_props = { "count": None, - "key": "value", "metadata": None, "version": None, } + self.kwargs["generic_dict"] = { + **base_enrollment_props, + "key": "value", + } + self.kwargs["twin_array_dict"] = { + **base_enrollment_props, + "values": [{"key1": "value1"}, {"key2": "value2"}], + } _cleanup_enrollments(self, dps, rg) @@ -553,3 +560,91 @@ def test_dps_enrollment_group_lifecycle(self): rg, dps, cert_name, cert_etag ) ) + + def test_dps_enrollment_twin_array(self): + hub_host_name = "{}.azure-devices.net".format(hub) + + # test twin array in enrollment + attestation_type = AttestationType.x509.value + device_id = self.create_random_name("device-id-for-test", length=48) + enrollment_id = self.create_random_name("enrollment-for-test", length=48) + + self.cmd( + "iot dps enrollment create --enrollment-id {} --attestation-type {}" + " -g {} --dps-name {} --cp {} --scp {}" + " --provisioning-status {} --device-id {}" + " --initial-twin-tags {} --initial-twin-properties {}" + " --allocation-policy {} --iot-hubs {}".format( + enrollment_id, + attestation_type, + rg, + dps, + cert_path, + cert_path, + provisioning_status, + device_id, + '"{generic_dict}"', + '"{twin_array_dict}"', + AllocationType.hashed.value, + hub_host_name, + ), + checks=[ + self.check("attestation.type", attestation_type), + self.check("registrationId", enrollment_id), + self.check("provisioningStatus", provisioning_status), + self.check("deviceId", device_id), + self.check("allocationPolicy", AllocationType.hashed.value), + self.check("iotHubs", hub_host_name.split()), + self.check("initialTwin.tags", self.kwargs["generic_dict"]), + self.check( + "initialTwin.properties.desired", self.kwargs["twin_array_dict"] + ), + self.exists("reprovisionPolicy"), + self.check("reprovisionPolicy.migrateDeviceData", True), + self.check("reprovisionPolicy.updateHubAssignment", True), + ], + ) + + self.cmd( + "iot dps enrollment delete -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_id + ) + ) + + # test twin array in enrollment group + enrollment_group_id = self.create_random_name("enrollment-for-test", length=48) + + self.cmd( + "iot dps enrollment-group create --enrollment-id {} -g {} --dps-name {}" + " --cp {} --scp {} --provisioning-status {} --allocation-policy {}" + " --iot-hubs {} --edge-enabled --props {}".format( + enrollment_group_id, + rg, + dps, + cert_path, + cert_path, + provisioning_status, + "geoLatency", + hub_host_name, + '"{twin_array_dict}"', + ), + checks=[ + self.check("enrollmentGroupId", enrollment_group_id), + self.check("provisioningStatus", provisioning_status), + self.exists("reprovisionPolicy"), + self.check("allocationPolicy", "geoLatency"), + self.check("iotHubs", hub_host_name.split()), + self.check( + "initialTwin.properties.desired", self.kwargs["twin_array_dict"] + ), + self.check("reprovisionPolicy.migrateDeviceData", True), + self.check("reprovisionPolicy.updateHubAssignment", True), + self.check("capabilities.iotEdge", True), + ], + ) + + self.cmd( + "iot dps enrollment-group delete -g {} --dps-name {} --enrollment-id {}".format( + rg, dps, enrollment_group_id + ) + ) diff --git a/azext_iot/tests/dps/test_iot_dps_unit.py b/azext_iot/tests/dps/test_iot_dps_unit.py index bfe308921..672fede11 100644 --- a/azext_iot/tests/dps/test_iot_dps_unit.py +++ b/azext_iot/tests/dps/test_iot_dps_unit.py @@ -182,6 +182,15 @@ def serviceclient_generic_error(self, mocked_response, fixture_gdcs, fixture_sas primary_key='primarykey', secondary_key='secondarykey', edge_enabled=True)), + (generate_enrollment_create_req(attestation_type='symmetricKey', + primary_key='primarykey', + secondary_key='secondarykey', + edge_enabled=True, + initial_twin_properties={'key': ['value1', 'value2']})), + (generate_enrollment_create_req(attestation_type='tpm', + endorsement_key='mykey', + provisioning_status='enabled', + initial_twin_properties={'key': ['value1', 'value2']})) ]) def test_enrollment_create(self, serviceclient, fixture_cmd, req): subject.iot_dps_device_enrollment_create(fixture_cmd, @@ -705,6 +714,14 @@ def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): (generate_enrollment_group_create_req(primary_key='primarykey', secondary_key='secondarykey', edge_enabled=True)), + (generate_enrollment_group_create_req(primary_key='primarykey', + secondary_key='secondarykey', + edge_enabled=True, + initial_twin_properties={'key': ['value1', 'value2']})), + (generate_enrollment_group_create_req(primary_key='primarykey', + secondary_key='secondarykey', + edge_enabled=True, + initial_twin_properties={'key': ['value1', 'value2']})), ]) def test_enrollment_group_create(self, serviceclient, req): subject.iot_dps_device_enrollment_group_create(None, From fd5c0495bd3a652c3443f8943168ef3def38a909 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Mon, 25 Jan 2021 11:21:20 -0800 Subject: [PATCH 165/179] Fix for device-identity update ignoring key/thumbprint values if no auth_method provided (#297) --- azext_iot/operations/hub.py | 31 +++++++++++++++++++ azext_iot/tests/iothub/test_iot_ext_unit.py | 33 +++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index bceb2f9f3..b85a8022f 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -295,6 +295,8 @@ def update_iot_device_custom( instance["status"] = status if status_reason is not None: instance["statusReason"] = status_reason + + auth_type = instance['authentication']['type'] if auth_method is not None: if auth_method == DeviceAuthType.shared_private_key.name: auth = "sas" @@ -323,6 +325,35 @@ def update_iot_device_custom( 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": + if any([primary_thumbprint, secondary_thumbprint]): + raise ValueError( + "Device authorization method {} does not support primary or secondary thumbprints.".format( + DeviceAuthType.shared_private_key.name + ) + ) + if primary_key: + instance["authentication"]["symmetricKey"]["primaryKey"] = primary_key + if secondary_key: + instance["authentication"]["symmetricKey"]["secondaryKey"] = secondary_key + + elif auth_type == "selfSigned": + if any([primary_key, secondary_key]): + raise ValueError( + "Device authorization method {} does not support primary or secondary keys.".format( + DeviceAuthType.x509_thumbprint.name + ) + ) + if primary_thumbprint: + instance["authentication"]["x509Thumbprint"][ + "primaryThumbprint" + ] = primary_thumbprint + if secondary_thumbprint: + instance["authentication"]["x509Thumbprint"][ + "secondaryThumbprint" + ] = secondary_thumbprint return instance diff --git a/azext_iot/tests/iothub/test_iot_ext_unit.py b/azext_iot/tests/iothub/test_iot_ext_unit.py index f28bd86db..7f701a442 100644 --- a/azext_iot/tests/iothub/test_iot_ext_unit.py +++ b/azext_iot/tests/iothub/test_iot_ext_unit.py @@ -520,6 +520,20 @@ def test_device_update(self, fixture_cmd, serviceclient, req): ), ), (generate_device_show(), device_update_con_arg(auth_method="x509_ca",)), + (generate_device_show(), device_update_con_arg(primary_key="secondary_key", secondary_key="primary_key")), + ( + generate_device_show( + authentication={ + "type": "selfSigned", + "symmetricKey": {"primaryKey": None, "secondaryKey": None}, + "x509Thumbprint": { + "primaryThumbprint": "123", + "secondaryThumbprint": "321", + }, + } + ), + device_update_con_arg(primary_thumbprint="321", secondary_thumbprint="123") + ) ], ) def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): @@ -583,6 +597,25 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): device_update_con_arg(auth_method="Unknown",), ValueError, ), + ( + generate_device_show(), + device_update_con_arg(primary_thumbprint="newThumbprint",), + ValueError, + ), + ( + generate_device_show( + authentication={ + "type": "selfSigned", + "symmetricKey": {"primaryKey": None, "secondaryKey": None}, + "x509Thumbprint": { + "primaryThumbprint": "123", + "secondaryThumbprint": "321", + }, + } + ), + device_update_con_arg(primary_key='updated_key'), + ValueError + ) ], ) def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): From 5e6416d4b57e94e9cb5d64f576eeb3322cb49242 Mon Sep 17 00:00:00 2001 From: Paul Montgomery Date: Wed, 27 Jan 2021 11:32:27 -0800 Subject: [PATCH 166/179] Fix base64 encoding (#302) * Fix base64 encoding issues (binary string - regular string) * Update version and History * PR feedback --- HISTORY.rst | 7 +++++++ azext_iot/constants.py | 2 +- azext_iot/product/test/command_tests.py | 5 ++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 289c87a00..d0237e16c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,13 @@ Release History =============== +0.10.9 ++++++++++++++++ + +**Azure IoT Product Certification service** + +* Fix bug for `az iot product test create` sending a byte string instead of "regular" base64 string. + 0.10.8 +++++++++++++++ diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 4e30e223b..8b8f419db 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.8" +VERSION = "0.10.9" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/product/test/command_tests.py b/azext_iot/product/test/command_tests.py index 7d73a351c..3e5cb9948 100644 --- a/azext_iot/product/test/command_tests.py +++ b/azext_iot/product/test/command_tests.py @@ -278,9 +278,8 @@ def _read_certificate_from_file(certificate_path): with open(file=certificate_path, mode="rb") as f: data = f.read() - from base64 import encodestring # pylint: disable=no-name-in-module - - return encodestring(data) + from base64 import b64encode # pylint: disable=no-name-in-module + return b64encode(data).decode('utf-8') def _create_from_file(configuration_file): From a808ed9926c020695fc3eb7bcf9181df8e3655f3 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 28 Jan 2021 12:50:10 -0800 Subject: [PATCH 167/179] Digital twins staging to dev (#305) * ADT CLI features and functionality around managed identity. (#295) * ADT CLI features and functionality around private endpoints + links. (#301) * Includes significant DT CLI IT tooling upgrades. * Simplify DT client init. (#304) * Debug logging improvements. * Update HISTORY.rst --- HISTORY.rst | 14 +- azext_iot/_factory.py | 6 +- azext_iot/common/embedded_cli.py | 14 +- azext_iot/digitaltwins/_help.py | 153 ++++++- azext_iot/digitaltwins/command_map.py | 35 +- azext_iot/digitaltwins/commands_resource.py | 196 ++++----- azext_iot/digitaltwins/common.py | 31 +- azext_iot/digitaltwins/params.py | 115 +++++- azext_iot/digitaltwins/providers/__init__.py | 10 +- azext_iot/digitaltwins/providers/base.py | 26 +- .../providers/endpoint/__init__.py | 5 + .../providers/endpoint/builders.py | 330 +++++++++++++++ azext_iot/digitaltwins/providers/rbac.py | 11 + azext_iot/digitaltwins/providers/resource.py | 317 ++++++++++----- azext_iot/digitaltwins/providers/twin.py | 4 +- .../azure_digital_twins_management_client.py | 20 +- .../controlplane/models/__init__.py | 52 +++ ...e_digital_twins_management_client_enums.py | 35 ++ .../controlplane/models/check_name_result.py | 2 +- .../models/check_name_result_py3.py | 2 +- .../models/connection_properties.py | 52 +++ .../connection_properties_private_endpoint.py | 34 ++ ...nection_properties_private_endpoint_py3.py | 34 ++ ...s_private_link_service_connection_state.py | 44 ++ ...ivate_link_service_connection_state_py3.py | 44 ++ .../models/connection_properties_py3.py | 52 +++ .../controlplane/models/connection_state.py | 47 +++ .../models/connection_state_py3.py | 47 +++ .../models/digital_twins_description.py | 21 +- .../models/digital_twins_description_paged.py | 2 +- .../models/digital_twins_description_py3.py | 25 +- .../models/digital_twins_endpoint_resource.py | 2 +- .../digital_twins_endpoint_resource_paged.py | 2 +- ...ital_twins_endpoint_resource_properties.py | 17 +- ..._twins_endpoint_resource_properties_py3.py | 19 +- .../digital_twins_endpoint_resource_py3.py | 2 +- .../models/digital_twins_identity.py | 50 +++ .../models/digital_twins_identity_py3.py | 50 +++ .../models/digital_twins_patch_description.py | 10 +- .../digital_twins_patch_description_py3.py | 12 +- .../models/digital_twins_patch_properties.py | 30 ++ .../digital_twins_patch_properties_py3.py | 30 ++ .../models/digital_twins_resource.py | 4 + .../models/digital_twins_resource_py3.py | 6 +- .../controlplane/models/error_definition.py | 2 +- .../models/error_definition_py3.py | 2 +- .../controlplane/models/error_response.py | 2 +- .../controlplane/models/error_response_py3.py | 2 +- .../controlplane/models/event_grid.py | 15 +- .../controlplane/models/event_grid_py3.py | 19 +- .../controlplane/models/event_hub.py | 32 +- .../controlplane/models/event_hub_py3.py | 36 +- .../models/group_id_information.py | 51 +++ .../models/group_id_information_properties.py | 37 ++ .../group_id_information_properties_model.py | 34 ++ ...oup_id_information_properties_model_py3.py | 34 ++ .../group_id_information_properties_py3.py | 37 ++ .../models/group_id_information_py3.py | 51 +++ .../models/group_id_information_response.py | 29 ++ .../group_id_information_response_py3.py | 29 ++ .../controlplane/models/operation.py | 2 +- .../controlplane/models/operation_paged.py | 2 +- .../controlplane/models/operation_py3.py | 2 +- .../controlplane/models/private_endpoint.py | 35 ++ .../models/private_endpoint_connection.py | 52 +++ .../private_endpoint_connection_properties.py | 48 +++ ...vate_endpoint_connection_properties_py3.py | 48 +++ .../models/private_endpoint_connection_py3.py | 52 +++ .../private_endpoint_connections_response.py | 29 ++ ...ivate_endpoint_connections_response_py3.py | 29 ++ .../models/private_endpoint_py3.py | 35 ++ .../controlplane/models/service_bus.py | 32 +- .../controlplane/models/service_bus_py3.py | 36 +- .../controlplane/operations/__init__.py | 4 + .../digital_twins_endpoint_operations.py | 36 +- .../operations/digital_twins_operations.py | 141 ++++--- .../controlplane/operations/operations.py | 8 +- ...private_endpoint_connections_operations.py | 364 +++++++++++++++++ .../private_link_resources_operations.py | 164 ++++++++ .../sdk/digitaltwins/controlplane/version.py | 2 +- .../dataplane/azure_digital_twins_api.py | 3 - .../service/provisioning_service_client.py | 2 - .../device/iot_hub_gateway_device_ap_is.py | 2 - .../service/iot_hub_gateway_service_ap_is.py | 2 - azext_iot/tests/digitaltwins/__init__.py | 140 +++++-- .../test_dt_model_lifecycle_int.py | 39 +- .../test_dt_privatelinks_lifecycle_int.py | 176 ++++++++ .../test_dt_resource_lifecycle_int.py | 380 ++++++++++++------ .../test_dt_twin_lifecycle_int.py | 51 ++- pytest.ini.example | 1 + 90 files changed, 3747 insertions(+), 596 deletions(-) create mode 100644 azext_iot/digitaltwins/providers/endpoint/__init__.py create mode 100644 azext_iot/digitaltwins/providers/endpoint/builders.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/connection_properties.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_endpoint.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_endpoint_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_link_service_connection_state.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_link_service_connection_state_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/connection_state.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/connection_state_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_identity.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_identity_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_properties.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_properties_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/group_id_information.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_model.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_model_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_response.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_response_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_properties.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_properties_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connections_response.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connections_response_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_py3.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/operations/private_endpoint_connections_operations.py create mode 100644 azext_iot/sdk/digitaltwins/controlplane/operations/private_link_resources_operations.py create mode 100644 azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py diff --git a/HISTORY.rst b/HISTORY.rst index d0237e16c..79f82ed08 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,10 +6,22 @@ Release History 0.10.9 +++++++++++++++ -**Azure IoT Product Certification service** +**Azure IoT Product Certification service updates** * Fix bug for `az iot product test create` sending a byte string instead of "regular" base64 string. +**Azure Digital Twins updates** + +* Addition of Digital Twins Identity support focused around Managed Service Identity (MSI) and Identity based endpoint integration. +* Addition of Digital Twins networking functionality around private-links and private-endpoint connections. See "az dt network". + +**IoT Hub updates** + +* Improve http debug logging. +* Fix bug related to issue #296. Adds a clause to device-identity update that allows user to update primary-key / secondary-key +and primary-thumbprint / secondary-thumbprint values (respectively, per auth method) without needing to specify the auth_method in the update command. + + 0.10.8 +++++++++++++++ diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py index 9915c410e..98fcbbf44 100644 --- a/azext_iot/_factory.py +++ b/azext_iot/_factory.py @@ -10,6 +10,7 @@ from azext_iot.common.sas_token_auth import SasTokenAuthentication from azext_iot.common.shared import SdkType +from azext_iot.constants import USER_AGENT from msrestazure.azure_exceptions import CloudError __all__ = [ @@ -70,7 +71,10 @@ def __init__(self, target, device_id=None, auth_override=None): def get_sdk(self, sdk_type): sdk_map = self._construct_sdk_map() - return sdk_map[sdk_type]() + sdk_client = sdk_map[sdk_type]() + sdk_client.config.enable_http_logger = True + sdk_client.config.add_user_agent(USER_AGENT) + return sdk_client def _construct_sdk_map(self): return { diff --git a/azext_iot/common/embedded_cli.py b/azext_iot/common/embedded_cli.py index 2fa7a8826..5e5ab471a 100644 --- a/azext_iot/common/embedded_cli.py +++ b/azext_iot/common/embedded_cli.py @@ -23,14 +23,22 @@ def __init__(self): def invoke(self, command: str, subscription: str = None): output_file = StringIO() + command = self._ensure_json_output(command=command) if subscription: command = self._ensure_subscription( command=command, subscription=subscription ) - self.error_code = ( - self.az_cli.invoke(shlex.split(command), out_file=output_file) or 0 - ) + + # TODO: Capture stderr? + try: + self.error_code = ( + self.az_cli.invoke(shlex.split(command), out_file=output_file) or 0 + ) + except SystemExit as se: + # Support caller error handling + self.error_code = se.code + self.output = output_file.getvalue() logger.debug( "Embedded CLI received error code: %s, output: '%s'", diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index 67fc1a77a..10dd5c1ae 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -22,13 +22,34 @@ def load_digitaltwins_help(): short-summary: Create a new Digital Twins instance. examples: - - name: Create instance in target resource group with default location. + - name: Create instance in target resource group using the resource group location. text: > - az dt create -n {instance_name} -g {resouce_group} -l eastus2euap + az dt create -n {instance_name} -g {resouce_group} - name: Create instance in target resource group with specified location and tags. text: > - az dt create -n {instance_name} -g {resouce_group} -l westcentralus --tags a=b c=d + az dt create -n {instance_name} -g {resouce_group} -l westus --tags a=b c=d + + - name: Create instance in the target resource group with a system managed identity. + text: > + az dt create -n {instance_name} -g {resouce_group} --assign-identity + + - name: Create instance in the target resource group with a system managed identity then + assign the identity to one or more scopes (space-separated) with the role of Contributor. + text: > + az dt create -n {instance_name} -g {resouce_group} --assign-identity + --scopes + "/subscriptions/a12345ea-bb21-994d-2263-c716348e32a1/resourceGroups/ProResourceGroup/providers/Microsoft.EventHub/namespaces/myEventHubNamespace/eventhubs/myEventHub" + "/subscriptions/a12345ea-bb21-994d-2263-c716348e32a1/resourceGroups/ProResourceGroup/providers/Microsoft.ServiceBus/namespaces/myServiceBusNamespace/topics/myTopic" + + - name: Create instance in the target resource group with a system managed identity then + assign the identity to one or more scopes with a custom specified role. + text: > + az dt create -n {instance_name} -g {resouce_group} --assign-identity + --scopes + "/subscriptions/a12345ea-bb21-994d-2263-c716348e32a1/resourceGroups/ProResourceGroup/providers/Microsoft.EventHub/namespaces/myEventHubNamespace/eventhubs/myEventHub" + "/subscriptions/a12345ea-bb21-994d-2263-c716348e32a1/resourceGroups/ProResourceGroup/providers/Microsoft.ServiceBus/namespaces/myServiceBusNamespace/topics/myTopic" + --role MyCustomRole """ helps["dt show"] = """ @@ -72,9 +93,12 @@ def load_digitaltwins_help(): short-summary: Delete an existing Digital Twins instance. examples: - - name: Delete an arbitrary instance. + - name: Delete an arbitrary instance in blocking fashion with a confirmation prompt. text: > az dt delete -n {instance_name} + - name: Delete an arbitrary instance with no blocking or prompt. + text: > + az dt delete -n {instance_name} -y --no-wait """ helps["dt endpoint"] = """ @@ -104,10 +128,11 @@ def load_digitaltwins_help(): helps["dt endpoint create eventhub"] = """ type: command short-summary: Adds an EventHub endpoint to a Digital Twins instance. - Requires pre-created resource. + Requires pre-created resource. The instance must be created + with a managed identity to support identity based endpoint integration examples: - - name: Adds an EventHub endpoint to a target instance. + - name: Adds an EventHub endpoint to a target instance using Key based auth. text: > az dt endpoint create eventhub --endpoint-name {endpoint_name} --eventhub-resource-group {eventhub_resource_group} @@ -115,15 +140,25 @@ def load_digitaltwins_help(): --eventhub {eventhub_name} --eventhub-policy {eventhub_policy} -n {instance_name} + + - name: Adds an EventHub endpoint to a target instance using Identity based auth. + text: > + az dt endpoint create eventhub --endpoint-name {endpoint_name} + --eventhub-resource-group {eventhub_resource_group} + --eventhub-namespace {eventhub_namespace} + --eventhub {eventhub_name} + --auth-type IdentityBased + -n {instance_name} """ helps["dt endpoint create servicebus"] = """ type: command short-summary: Adds a ServiceBus Topic endpoint to a Digital Twins instance. - Requires pre-created resource. + Requires pre-created resource. The instance must be created + with a managed identity to support identity based endpoint integration examples: - - name: Adds a ServiceBus Topic endpoint to a target instance. + - name: Adds a ServiceBus Topic endpoint to a target instance using Key based auth. text: > az dt endpoint create servicebus --endpoint-name {endpoint_name} --servicebus-resource-group {servicebus_resource_group} @@ -131,6 +166,14 @@ def load_digitaltwins_help(): --servicebus-topic {servicebus_topic_name} --servicebus-policy {servicebus_policy} -n {instance_name} + + - name: Adds a ServiceBus Topic endpoint to a target instance using Identity based auth. + text: > + az dt endpoint create servicebus --endpoint-name {endpoint_name} + --servicebus-resource-group {servicebus_resource_group} + --servicebus-namespace {servicebus_namespace} + --servicebus-topic {servicebus_topic_name} + -n {instance_name} """ helps["dt endpoint list"] = """ @@ -158,9 +201,101 @@ def load_digitaltwins_help(): short-summary: Remove an endpoint from a Digital Twins instance. examples: - - name: Remove an endpoint from an instance. + - name: Remove an endpoint from an instance and block until the operation is complete. text: > az dt endpoint delete -n {instance_name} --endpoint-name {endpoint_name} + - name: Remove an endpoint from an instance without confirmation or blocking. + text: > + az dt endpoint delete -n {instance_name} --endpoint-name {endpoint_name} -y --no-wait + """ + + helps["dt network"] = """ + type: group + short-summary: Manage Digital Twins network configuration including private links and endpoint connections. + """ + + helps["dt network private-link"] = """ + type: group + short-summary: Manage Digital Twins instance private-link operations. + """ + + helps["dt network private-link show"] = """ + type: command + short-summary: Show a private-link associated with the instance. + + examples: + - name: Show the private-link named "API" associated with the instance. + text: > + az dt network private-link show -n {instance_name} --link-name API + """ + + helps["dt network private-link list"] = """ + type: command + short-summary: List private-links associated with the Digital Twins instance. + + examples: + - name: List all private-links associated with the instance. + text: > + az dt network private-link list -n {instance_name} + """ + + helps["dt network private-endpoint"] = """ + type: group + short-summary: Manage Digital Twins instance private-endpoints. + long-summary: Use 'az network private-endpoint create' to create a private-endpoint and link to a Digital Twins resource. + """ + + helps["dt network private-endpoint connection"] = """ + type: group + short-summary: Manage Digital Twins instance private-endpoint connections. + """ + + helps["dt network private-endpoint connection list"] = """ + type: command + short-summary: List private-endpoint connections associated with the Digital Twins instance. + + examples: + - name: List all private-endpoint connections associated with the instance. + text: > + az dt network private-endpoint connection list -n {instance_name} + """ + + helps["dt network private-endpoint connection show"] = """ + type: command + short-summary: Show a private-endpoint connection associated with the Digital Twins instance. + + examples: + - name: Show details of the private-endpoint connection named ba8408b6-1372-41b2-aef8-af43afc4729f. + text: > + az dt network private-endpoint connection show -n {instance_name} --cn ba8408b6-1372-41b2-aef8-af43afc4729f + """ + + helps["dt network private-endpoint connection set"] = """ + type: command + short-summary: Set the state of a private-endpoint connection associated with the Digital Twins instance. + + examples: + - 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." + """ + + helps["dt network private-endpoint connection delete"] = """ + type: command + short-summary: Delete a private-endpoint connection associated with the Digital Twins instance. + + examples: + - name: Delete the private-endpoint connection named ba8408b6-1372-41b2-aef8-af43afc4729f with confirmation. Block until finished. + text: > + az dt network private-endpoint connection delete -n {instance_name} --cn ba8408b6-1372-41b2-aef8-af43afc4729f + + - name: Delete the private-endpoint connection named ba8408b6-1372-41b2-aef8-af43afc4729f no confirmation. Return immediately. + text: > + az dt network private-endpoint connection delete -n {instance_name} --cn ba8408b6-1372-41b2-aef8-af43afc4729f -y --no-wait """ helps["dt role-assignment"] = """ diff --git a/azext_iot/digitaltwins/command_map.py b/azext_iot/digitaltwins/command_map.py index c2cc25020..3b7df0b27 100644 --- a/azext_iot/digitaltwins/command_map.py +++ b/azext_iot/digitaltwins/command_map.py @@ -4,8 +4,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azure.cli.core.profiles import ResourceType - """ Load CLI commands """ @@ -39,12 +37,11 @@ def load_digitaltwins_commands(self, _): with self.command_group( "dt", command_type=digitaltwins_resource_ops, - resource_type=ResourceType.MGMT_RESOURCE_RESOURCES, ) as cmd_group: cmd_group.command("create", "create_instance") cmd_group.show_command("show", "show_instance") cmd_group.command("list", "list_instances") - cmd_group.command("delete", "delete_instance") + cmd_group.command("delete", "delete_instance", confirmation=True, supports_no_wait=True) with self.command_group( "dt endpoint", command_type=digitaltwins_resource_ops @@ -65,7 +62,7 @@ def load_digitaltwins_commands(self, _): "ProvisioningState:properties.provisioningState,CreatedTime:properties.createdTime}" ), ) - cmd_group.command("delete", "delete_endpoint") + cmd_group.command("delete", "delete_endpoint", confirmation=True, supports_no_wait=True) with self.command_group( "dt endpoint create", command_type=digitaltwins_resource_ops @@ -140,3 +137,31 @@ def load_digitaltwins_commands(self, _): ) cmd_group.command("update", "update_model") cmd_group.command("delete", "delete_model") + + with self.command_group( + "dt network", + command_type=digitaltwins_resource_ops, + ) as cmd_group: + pass + + with self.command_group( + "dt network private-link", + command_type=digitaltwins_resource_ops, + ) as cmd_group: + cmd_group.show_command("show", "show_private_link") + cmd_group.command("list", "list_private_links") + + with self.command_group( + "dt network private-endpoint", + command_type=digitaltwins_resource_ops, + ) as cmd_group: + pass + + with self.command_group( + "dt network private-endpoint connection", + command_type=digitaltwins_resource_ops, + ) as cmd_group: + cmd_group.command("set", "set_private_endpoint_conn") + 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) diff --git a/azext_iot/digitaltwins/commands_resource.py b/azext_iot/digitaltwins/commands_resource.py index aa1b0b58b..96d176bc0 100644 --- a/azext_iot/digitaltwins/commands_resource.py +++ b/azext_iot/digitaltwins/commands_resource.py @@ -5,16 +5,37 @@ # -------------------------------------------------------------------------------------------- from azext_iot.digitaltwins.providers.resource import ResourceProvider -from azext_iot.digitaltwins.common import ADTEndpointType +from azext_iot.digitaltwins.common import ( + ADTEndpointType, + ADTEndpointAuthType, + ADTPublicNetworkAccessType, +) from knack.log import get_logger logger = get_logger(__name__) -def create_instance(cmd, name, resource_group_name, location=None, tags=None): +def create_instance( + cmd, + name, + resource_group_name, + location=None, + tags=None, + assign_identity=None, + scopes=None, + role_type="Contributor", + public_network_access=ADTPublicNetworkAccessType.enabled.value, +): rp = ResourceProvider(cmd) return rp.create( - name=name, resource_group_name=resource_group_name, location=location, tags=tags + name=name, + resource_group_name=resource_group_name, + location=location, + tags=tags, + assign_identity=assign_identity, + scopes=scopes, + role_type=role_type, + public_network_access=public_network_access, ) @@ -63,44 +84,22 @@ def add_endpoint_eventgrid( eventgrid_resource_group, resource_group_name=None, endpoint_subscription=None, - dead_letter_endpoint=None, - tags=None, -): - return _add_endpoint_eventgrid( - cmd=cmd, - name=name, - endpoint_name=endpoint_name, - eventgrid_resource_group=eventgrid_resource_group, - eventgrid_topic_name=eventgrid_topic_name, - resource_group_name=resource_group_name, - endpoint_subscription=endpoint_subscription, - dead_letter_endpoint=dead_letter_endpoint, - tags=tags, - ) - - -def _add_endpoint_eventgrid( - cmd, - name, - endpoint_name, - eventgrid_topic_name, - eventgrid_resource_group, - resource_group_name=None, - endpoint_subscription=None, - dead_letter_endpoint=None, - tags=None, + dead_letter_uri=None, + dead_letter_secret=None, + auth_type=ADTEndpointAuthType.keybased.value, ): rp = ResourceProvider(cmd) return rp.add_endpoint( name=name, resource_group_name=resource_group_name, endpoint_name=endpoint_name, - endpoint_resource_type=ADTEndpointType.eventgridtopic, + endpoint_resource_type=ADTEndpointType.eventgridtopic.value, endpoint_resource_name=eventgrid_topic_name, endpoint_resource_group=eventgrid_resource_group, endpoint_subscription=endpoint_subscription, - dead_letter_endpoint=dead_letter_endpoint, - tags=tags, + dead_letter_uri=dead_letter_uri, + dead_letter_secret=dead_letter_secret, + auth_type=auth_type, ) @@ -110,54 +109,28 @@ def add_endpoint_servicebus( endpoint_name, servicebus_topic_name, servicebus_resource_group, - servicebus_policy, - servicebus_namespace, - resource_group_name=None, - endpoint_subscription=None, - dead_letter_endpoint=None, - tags=None, -): - return _add_endpoint_servicebus( - cmd=cmd, - name=name, - endpoint_name=endpoint_name, - servicebus_topic_name=servicebus_topic_name, - servicebus_resource_group=servicebus_resource_group, - servicebus_policy=servicebus_policy, - servicebus_namespace=servicebus_namespace, - resource_group_name=resource_group_name, - endpoint_subscription=endpoint_subscription, - dead_letter_endpoint=dead_letter_endpoint, - tags=tags, - ) - - -def _add_endpoint_servicebus( - cmd, - name, - endpoint_name, - servicebus_topic_name, - servicebus_resource_group, - servicebus_policy, servicebus_namespace, + servicebus_policy=None, resource_group_name=None, endpoint_subscription=None, - dead_letter_endpoint=None, - tags=None, + dead_letter_uri=None, + dead_letter_secret=None, + auth_type=ADTEndpointAuthType.keybased.value, ): rp = ResourceProvider(cmd) return rp.add_endpoint( name=name, resource_group_name=resource_group_name, endpoint_name=endpoint_name, - endpoint_resource_type=ADTEndpointType.servicebus, + endpoint_resource_type=ADTEndpointType.servicebus.value, endpoint_resource_name=servicebus_topic_name, endpoint_resource_group=servicebus_resource_group, endpoint_resource_namespace=servicebus_namespace, endpoint_resource_policy=servicebus_policy, endpoint_subscription=endpoint_subscription, - dead_letter_endpoint=dead_letter_endpoint, - tags=tags, + dead_letter_uri=dead_letter_uri, + dead_letter_secret=dead_letter_secret, + auth_type=auth_type, ) @@ -167,52 +140,81 @@ def add_endpoint_eventhub( endpoint_name, eventhub_name, eventhub_resource_group, - eventhub_policy, eventhub_namespace, + eventhub_policy=None, resource_group_name=None, endpoint_subscription=None, - dead_letter_endpoint=None, - tags=None, + dead_letter_uri=None, + dead_letter_secret=None, + auth_type=ADTEndpointAuthType.keybased.value, ): - return _add_endpoint_eventhub( - cmd=cmd, + rp = ResourceProvider(cmd) + return rp.add_endpoint( name=name, - endpoint_name=endpoint_name, - eventhub_name=eventhub_name, - eventhub_resource_group=eventhub_resource_group, - eventhub_policy=eventhub_policy, - eventhub_namespace=eventhub_namespace, resource_group_name=resource_group_name, + endpoint_name=endpoint_name, + endpoint_resource_type=ADTEndpointType.eventhub.value, + endpoint_resource_name=eventhub_name, + endpoint_resource_group=eventhub_resource_group, + endpoint_resource_namespace=eventhub_namespace, + endpoint_resource_policy=eventhub_policy, endpoint_subscription=endpoint_subscription, - dead_letter_endpoint=dead_letter_endpoint, - tags=tags, + dead_letter_uri=dead_letter_uri, + dead_letter_secret=dead_letter_secret, + auth_type=auth_type, ) -def _add_endpoint_eventhub( +def show_private_link(cmd, name, link_name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.get_private_link( + name=name, resource_group_name=resource_group_name, link_name=link_name + ) + + +def list_private_links(cmd, name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.list_private_links(name=name, resource_group_name=resource_group_name) + + +def set_private_endpoint_conn( cmd, name, - endpoint_name, - eventhub_name, - eventhub_resource_group, - eventhub_policy, - eventhub_namespace, + conn_name, + status, + description=None, + group_ids=None, + actions_required=None, resource_group_name=None, - endpoint_subscription=None, - dead_letter_endpoint=None, - tags=None, ): rp = ResourceProvider(cmd) - return rp.add_endpoint( + return rp.set_private_endpoint_conn( name=name, resource_group_name=resource_group_name, - endpoint_name=endpoint_name, - endpoint_resource_type=ADTEndpointType.eventhub, - endpoint_resource_name=eventhub_name, - endpoint_resource_group=eventhub_resource_group, - endpoint_resource_namespace=eventhub_namespace, - endpoint_resource_policy=eventhub_policy, - endpoint_subscription=endpoint_subscription, - dead_letter_endpoint=dead_letter_endpoint, - tags=tags, + conn_name=conn_name, + status=status, + description=description, + group_ids=group_ids, + actions_required=actions_required, + ) + + +def show_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 + ) + + +def list_private_endpoint_conns(cmd, name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.list_private_endpoint_conns( + name=name, resource_group_name=resource_group_name + ) + + +def delete_private_endpoint_conn(cmd, name, conn_name, resource_group_name=None): + rp = ResourceProvider(cmd) + return rp.delete_private_endpoint_conn( + name=name, resource_group_name=resource_group_name, conn_name=conn_name ) diff --git a/azext_iot/digitaltwins/common.py b/azext_iot/digitaltwins/common.py index aeb99af92..b0a382a70 100644 --- a/azext_iot/digitaltwins/common.py +++ b/azext_iot/digitaltwins/common.py @@ -14,9 +14,38 @@ class ADTEndpointType(Enum): """ - ADT Location Type. + ADT endpoint type. """ eventgridtopic = "eventgridtopic" servicebus = "servicebus" eventhub = "eventhub" + + +class ADTEndpointAuthType(Enum): + """ + ADT endpoint auth type. + """ + + identitybased = "IdentityBased" + keybased = "KeyBased" + + +class ADTPrivateConnectionStatusType(Enum): + """ + ADT private endpoint connection status type. + """ + + pending = "Pending" + approved = "Approved" + rejected = "Rejected" + disconnected = "Disconnected" + + +class ADTPublicNetworkAccessType(Enum): + """ + ADT private endpoint connection status type. + """ + + enabled = "Enabled" + disabled = "Disabled" diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index 90a1edfc4..f9a217107 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -12,7 +12,13 @@ from azure.cli.core.commands.parameters import ( resource_group_name_type, get_three_state_flag, - tags_type + get_enum_type, + tags_type, +) +from azext_iot.digitaltwins.common import ( + ADTEndpointAuthType, + ADTPrivateConnectionStatusType, + ADTPublicNetworkAccessType, ) depfor_type = CLIArgumentType( @@ -70,10 +76,14 @@ def load_digitaltwins_arguments(self, _): help="Event route name.", ) context.argument( - "filter", options_list=["--filter"], help="Event route filter.", + "filter", + options_list=["--filter"], + help="Event route filter.", ) context.argument( - "role_type", options_list=["--role"], help="Role name or Id.", + "role_type", + options_list=["--role"], + help="Role name or Id.", ) context.argument( "assignee", @@ -87,7 +97,9 @@ def load_digitaltwins_arguments(self, _): help="Digital Twins model Id. Example: dtmi:com:example:Room;2", ) context.argument( - "twin_id", options_list=["--twin-id", "-t"], help="The digital twin Id.", + "twin_id", + options_list=["--twin-id", "-t"], + help="The digital twin Id.", ) context.argument( "include_inherited", @@ -101,13 +113,53 @@ def load_digitaltwins_arguments(self, _): options_list=["--top"], help="Maximum number of elements to return.", ) + context.argument( + "public_network_access", + options_list=["--public-network-access", "--pna"], + help="Determines if the Digital Twins instance can be accessed from a public network.", + arg_group="Networking", + arg_type=get_enum_type(ADTPublicNetworkAccessType), + ) + + with self.argument_context("dt create") as context: + context.argument( + "assign_identity", + arg_group="Managed Service Identity", + help="Assign a system generated identity to the Digital Twins instance.", + arg_type=get_three_state_flag(), + ) + context.argument( + "scopes", + arg_group="Managed Service Identity", + nargs="+", + options_list=["--scopes"], + help="Space-seperated scopes the system assigned identity can access.", + ) + context.argument( + "role_type", + arg_group="Managed Service Identity", + options_list=["--role"], + help="Role name or Id the system assigned identity will have.", + ) with self.argument_context("dt endpoint create") as context: context.argument( - "dead_letter_endpoint", + "dead_letter_secret", options_list=["--deadletter-sas-uri", "--dsu"], - help="Dead-letter storage container URL with SAS token", - arg_group="Dead-letter Endpoint" + help="Dead-letter storage container URL with SAS token for Key based authentication.", + arg_group="Dead-letter Endpoint", + ) + context.argument( + "dead_letter_uri", + options_list=["--deadletter-uri", "--du"], + help="Dead-letter storage container URL for Identity based authentication.", + arg_group="Dead-letter Endpoint", + ) + context.argument( + "auth_type", + options_list=["--auth-type"], + help="Endpoint authentication type.", + arg_type=get_enum_type(ADTEndpointAuthType), ) with self.argument_context("dt endpoint create eventgrid") as context: @@ -141,7 +193,7 @@ def load_digitaltwins_arguments(self, _): context.argument( "eventhub_policy", options_list=["--eventhub-policy", "--ehp"], - help="EventHub policy to use for endpoint configuration.", + help="EventHub policy to use for endpoint configuration. Required when --auth-type is KeyBased.", arg_group="Event Hub", ) context.argument( @@ -174,7 +226,7 @@ def load_digitaltwins_arguments(self, _): context.argument( "servicebus_policy", options_list=["--servicebus-policy", "--sbp"], - help="ServiceBus Topic policy to use for endpoint configuration.", + help="ServiceBus Topic policy to use for endpoint configuration. Required when --auth-type is KeyBased.", arg_group="Service Bus Topic", ) context.argument( @@ -317,5 +369,48 @@ def load_digitaltwins_arguments(self, _): help="Indicates intent to decommission a target model.", ) context.argument( - "dependencies_for", arg_type=depfor_type, + "dependencies_for", + arg_type=depfor_type, + ) + + with self.argument_context("dt network private-link") as context: + context.argument( + "link_name", + options_list=["--link-name", "--ln"], + help="Private link name.", + arg_group="Private Connection", + ) + + with self.argument_context("dt network private-endpoint") as context: + context.argument( + "conn_name", + options_list=["--conn-name", "--cn"], + help="Private endpoint connection name.", + arg_group="Private-Endpoint", + ) + context.argument( + "group_ids", + options_list=["--group-ids"], + help="Space seperated list of group ids that the private endpoint should connect to.", + arg_group="Private-Endpoint", + nargs="+", + ) + context.argument( + "status", + options_list=["--status"], + help="The status of a private endpoint connection.", + arg_type=get_enum_type(ADTPrivateConnectionStatusType), + arg_group="Private-Endpoint", + ) + context.argument( + "description", + options_list=["--description", "--desc"], + help="Description for the private endpoint connection.", + arg_group="Private-Endpoint", + ) + context.argument( + "actions_required", + options_list=["--actions-required", "--ar"], + help="A message indicating if changes on the service provider require any updates on the consumer.", + arg_group="Private-Endpoint", ) diff --git a/azext_iot/digitaltwins/providers/__init__.py b/azext_iot/digitaltwins/providers/__init__.py index 274d3cfc4..c8e0929b6 100644 --- a/azext_iot/digitaltwins/providers/__init__.py +++ b/azext_iot/digitaltwins/providers/__init__.py @@ -7,6 +7,7 @@ from azext_iot.sdk.digitaltwins.controlplane import AzureDigitalTwinsManagementClient from azext_iot.sdk.digitaltwins.controlplane.models import ErrorResponseException from msrestazure.azure_exceptions import CloudError +from azext_iot.constants import USER_AGENT __all__ = [ "digitaltwins_service_factory", @@ -16,7 +17,7 @@ ] -def digitaltwins_service_factory(cli_ctx, *_): +def digitaltwins_service_factory(cli_ctx, *_) -> AzureDigitalTwinsManagementClient: """ Factory for importing deps and getting service client resources. @@ -25,8 +26,7 @@ def digitaltwins_service_factory(cli_ctx, *_): *_ : all other args ignored. Returns: - iot_hub_resource (IotHubClient.iot_hub_resource): operational resource for - working with IoT Hub. + AzureDigitalTwinsManagementClient: Top level client instance. """ from azure.cli.core.commands.client_factory import get_mgmt_service_client @@ -39,4 +39,6 @@ def __init__(self, cmd): self.cmd = cmd def get_mgmt_sdk(self): - return digitaltwins_service_factory(self.cmd.cli_ctx) + client = digitaltwins_service_factory(self.cmd.cli_ctx) + client.config.add_user_agent(USER_AGENT) + return client diff --git a/azext_iot/digitaltwins/providers/base.py b/azext_iot/digitaltwins/providers/base.py index c93d94aad..4670b92cb 100644 --- a/azext_iot/digitaltwins/providers/base.py +++ b/azext_iot/digitaltwins/providers/base.py @@ -5,10 +5,9 @@ # -------------------------------------------------------------------------------------------- from azext_iot.digitaltwins.providers.resource import ResourceProvider -from azext_iot.digitaltwins.providers.auth import DigitalTwinAuthentication from azext_iot.sdk.digitaltwins.dataplane import AzureDigitalTwinsAPI from azext_iot.sdk.digitaltwins.dataplane.models import ErrorResponseException -from azext_iot.constants import DIGITALTWINS_RESOURCE_ID +from azext_iot.constants import DIGITALTWINS_RESOURCE_ID, USER_AGENT from azext_iot.common.utility import valid_hostname from knack.cli import CLIError @@ -32,12 +31,14 @@ def _get_endpoint(self): http_prefix = "http://" if self.name.lower().startswith(https_prefix): - self.name = self.name[len(https_prefix):] + self.name = self.name[len(https_prefix) :] elif self.name.lower().startswith(http_prefix): - self.name = self.name[len(http_prefix):] + self.name = self.name[len(http_prefix) :] if not all([valid_hostname(self.name), "." in self.name]): - instance = self.rp.find_instance(name=self.name, resource_group_name=self.rg) + instance = self.rp.find_instance( + name=self.name, resource_group_name=self.rg + ) host_name = instance.host_name if not host_name: raise CLIError("Instance has invalid hostName. Aborting operation...") @@ -47,5 +48,16 @@ def _get_endpoint(self): return "https://{}".format(host_name) def get_sdk(self): - creds = DigitalTwinAuthentication(cmd=self.cmd, resource_id=self.resource_id) - return AzureDigitalTwinsAPI(base_url=self._get_endpoint(), credentials=creds) + from azure.cli.core.commands.client_factory import get_mgmt_service_client + + client = get_mgmt_service_client( + cli_ctx=self.cmd.cli_ctx, + client_or_resource_type=AzureDigitalTwinsAPI, + base_url=self._get_endpoint(), + resource=self.resource_id, + subscription_bound=False, + base_url_bound=False, + ) + + client.config.add_user_agent(USER_AGENT) + return client diff --git a/azext_iot/digitaltwins/providers/endpoint/__init__.py b/azext_iot/digitaltwins/providers/endpoint/__init__.py new file mode 100644 index 000000000..55614acbf --- /dev/null +++ b/azext_iot/digitaltwins/providers/endpoint/__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/digitaltwins/providers/endpoint/builders.py b/azext_iot/digitaltwins/providers/endpoint/builders.py new file mode 100644 index 000000000..73e05c581 --- /dev/null +++ b/azext_iot/digitaltwins/providers/endpoint/builders.py @@ -0,0 +1,330 @@ +# 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.common.embedded_cli import EmbeddedCLI +from azext_iot.digitaltwins.common import ADTEndpointAuthType +from abc import ABC, abstractmethod +from knack.util import CLIError +from knack.log import get_logger + +from azext_iot.sdk.digitaltwins.controlplane.models import ( + EventGrid as EventGridEndpointProperties, + EventHub as EventHubEndpointProperties, + ServiceBus as ServiceBusEndpointProperties, +) + +logger = get_logger(__name__) + + +class BaseEndpointBuilder(ABC): + def __init__( + self, + endpoint_resource_name: str, + endpoint_resource_group: str, + auth_type: str = ADTEndpointAuthType.keybased.value, + dead_letter_secret: str = None, + dead_letter_uri: str = None, + endpoint_subscription: str = None, + ): + self.cli = EmbeddedCLI() + self.error_prefix = "Could not create ADT instance endpoint. Unable to retrieve" + self.endpoint_resource_name = endpoint_resource_name + self.endpoint_resource_group = endpoint_resource_group + self.endpoint_subscription = endpoint_subscription + self.auth_type = auth_type + self.dead_letter_secret = dead_letter_secret + self.dead_letter_uri = dead_letter_uri + + def build_endpoint(self): + endpoint_properties = ( + self.build_key_based() + if self.auth_type == ADTEndpointAuthType.keybased.value + else self.build_identity_based() + ) + endpoint_properties.authentication_type = self.auth_type + return endpoint_properties + + @abstractmethod + def build_key_based(self): + pass + + @abstractmethod + def build_identity_based(self): + pass + + +class EventGridEndpointBuilder(BaseEndpointBuilder): + def __init__( + self, + endpoint_resource_name, + endpoint_resource_group, + auth_type=ADTEndpointAuthType.keybased.value, + dead_letter_secret=None, + dead_letter_uri=None, + endpoint_subscription=None, + ): + super().__init__( + endpoint_resource_name=endpoint_resource_name, + endpoint_resource_group=endpoint_resource_group, + auth_type=auth_type, + dead_letter_secret=dead_letter_secret, + dead_letter_uri=dead_letter_uri, + endpoint_subscription=endpoint_subscription, + ) + + def build_key_based(self): + eg_topic_keys_op = self.cli.invoke( + "eventgrid topic key list -n {} -g {}".format( + self.endpoint_resource_name, self.endpoint_resource_group + ), + subscription=self.endpoint_subscription, + ) + if not eg_topic_keys_op.success(): + raise CLIError("{} Event Grid topic keys.".format(self.error_prefix)) + eg_topic_keys = eg_topic_keys_op.as_json() + + eg_topic_endpoint_op = self.cli.invoke( + "eventgrid topic show -n {} -g {}".format( + self.endpoint_resource_name, self.endpoint_resource_group + ), + subscription=self.endpoint_subscription, + ) + if not eg_topic_endpoint_op.success(): + raise CLIError("{} Event Grid topic endpoint.".format(self.error_prefix)) + eg_topic_endpoint = eg_topic_endpoint_op.as_json() + + # TODO: Potentionally have shared attributes handled by build_endpoint() + return EventGridEndpointProperties( + access_key1=eg_topic_keys["key1"], + access_key2=eg_topic_keys["key2"], + dead_letter_secret=self.dead_letter_secret, + dead_letter_uri=self.dead_letter_uri, + topic_endpoint=eg_topic_endpoint["endpoint"], + ) + + def build_identity_based(self): + raise CLIError( + "Identity based EventGrid endpoint creation is not yet supported. " + ) + + +class ServiceBusEndpointBuilder(BaseEndpointBuilder): + def __init__( + self, + endpoint_resource_name, + endpoint_resource_group, + endpoint_resource_namespace, + endpoint_resource_policy, + auth_type=ADTEndpointAuthType.keybased.value, + dead_letter_secret=None, + dead_letter_uri=None, + endpoint_subscription=None, + ): + super().__init__( + endpoint_resource_name=endpoint_resource_name, + endpoint_resource_group=endpoint_resource_group, + auth_type=auth_type, + dead_letter_secret=dead_letter_secret, + dead_letter_uri=dead_letter_uri, + endpoint_subscription=endpoint_subscription, + ) + self.endpoint_resource_namespace = endpoint_resource_namespace + self.endpoint_resource_policy = endpoint_resource_policy + + def build_key_based(self): + sb_topic_keys_op = self.cli.invoke( + "servicebus topic authorization-rule keys list -n {} " + "--namespace-name {} -g {} --topic-name {}".format( + self.endpoint_resource_policy, + self.endpoint_resource_namespace, + self.endpoint_resource_group, + self.endpoint_resource_name, + ), + subscription=self.endpoint_subscription, + ) + if not sb_topic_keys_op.success(): + raise CLIError("{} Service Bus topic keys.".format(self.error_prefix)) + sb_topic_keys = sb_topic_keys_op.as_json() + + return ServiceBusEndpointProperties( + primary_connection_string=sb_topic_keys["primaryConnectionString"], + secondary_connection_string=sb_topic_keys["secondaryConnectionString"], + dead_letter_secret=self.dead_letter_secret, + dead_letter_uri=self.dead_letter_uri, + ) + + def build_identity_based(self): + sb_namespace_op = self.cli.invoke( + "servicebus namespace show --name {} -g {}".format( + self.endpoint_resource_namespace, + self.endpoint_resource_group, + ), + subscription=self.endpoint_subscription, + ) + if not sb_namespace_op.success(): + raise CLIError("{} Service Bus Namespace.".format(self.error_prefix)) + sb_namespace_meta = sb_namespace_op.as_json() + sb_endpoint = sb_namespace_meta["serviceBusEndpoint"] + + sb_topic_op = self.cli.invoke( + "servicebus topic show --name {} --namespace {} -g {}".format( + self.endpoint_resource_name, + self.endpoint_resource_namespace, + self.endpoint_resource_group, + ), + subscription=self.endpoint_subscription, + ) + + if not sb_topic_op.success(): + raise CLIError("{} Service Bus Topic.".format(self.error_prefix)) + + return ServiceBusEndpointProperties( + endpoint_uri=transform_sb_hostname_to_schemauri(sb_endpoint), + entity_path=self.endpoint_resource_name, + dead_letter_secret=self.dead_letter_secret, + dead_letter_uri=self.dead_letter_uri, + ) + + +class EventHubEndpointBuilder(BaseEndpointBuilder): + def __init__( + self, + endpoint_resource_name, + endpoint_resource_group, + endpoint_resource_namespace, + endpoint_resource_policy, + auth_type=ADTEndpointAuthType.keybased.value, + dead_letter_secret=None, + dead_letter_uri=None, + endpoint_subscription=None, + ): + super().__init__( + endpoint_resource_name=endpoint_resource_name, + endpoint_resource_group=endpoint_resource_group, + auth_type=auth_type, + dead_letter_secret=dead_letter_secret, + dead_letter_uri=dead_letter_uri, + endpoint_subscription=endpoint_subscription, + ) + self.endpoint_resource_namespace = endpoint_resource_namespace + self.endpoint_resource_policy = endpoint_resource_policy + + def build_key_based(self): + eventhub_topic_keys_op = self.cli.invoke( + "eventhubs eventhub authorization-rule keys list -n {} " + "--namespace-name {} -g {} --eventhub-name {}".format( + self.endpoint_resource_policy, + self.endpoint_resource_namespace, + self.endpoint_resource_group, + self.endpoint_resource_name, + ), + subscription=self.endpoint_subscription, + ) + if not eventhub_topic_keys_op.success(): + raise CLIError("{} Event Hub keys.".format(self.error_prefix)) + eventhub_topic_keys = eventhub_topic_keys_op.as_json() + + return EventHubEndpointProperties( + connection_string_primary_key=eventhub_topic_keys[ + "primaryConnectionString" + ], + connection_string_secondary_key=eventhub_topic_keys[ + "secondaryConnectionString" + ], + dead_letter_secret=self.dead_letter_secret, + dead_letter_uri=self.dead_letter_uri, + ) + + def build_identity_based(self): + sb_namespace_op = self.cli.invoke( + "eventhubs namespace show --name {} -g {}".format( + self.endpoint_resource_namespace, + self.endpoint_resource_group, + ), + subscription=self.endpoint_subscription, + ) + if not sb_namespace_op.success(): + raise CLIError("{} EventHub Namespace.".format(self.error_prefix)) + sb_namespace_meta = sb_namespace_op.as_json() + sb_endpoint = sb_namespace_meta["serviceBusEndpoint"] + + sb_topic_op = self.cli.invoke( + "eventhubs eventhub show --name {} --namespace {} -g {}".format( + self.endpoint_resource_name, + self.endpoint_resource_namespace, + self.endpoint_resource_group, + ), + subscription=self.endpoint_subscription, + ) + + if not sb_topic_op.success(): + raise CLIError("{} EventHub.".format(self.error_prefix)) + + return EventHubEndpointProperties( + endpoint_uri=transform_sb_hostname_to_schemauri(sb_endpoint), + entity_path=self.endpoint_resource_name, + dead_letter_secret=self.dead_letter_secret, + dead_letter_uri=self.dead_letter_uri, + ) + + +def transform_sb_hostname_to_schemauri(endpoint): + from urllib.parse import urlparse + + sb_endpoint_parts = urlparse(endpoint) + sb_hostname = sb_endpoint_parts.hostname + sb_schema_uri = "sb://{}/".format(sb_hostname) + return sb_schema_uri + + +def build_endpoint( + endpoint_resource_type: str, + endpoint_resource_name: str, + endpoint_resource_group: str, + auth_type: str = ADTEndpointAuthType.keybased.value, + endpoint_resource_namespace: str = None, + endpoint_resource_policy: str = None, + dead_letter_secret: str = None, + dead_letter_uri: str = None, + endpoint_subscription: str = None, +): + from azext_iot.digitaltwins.common import ADTEndpointType + + if endpoint_resource_type == ADTEndpointType.eventgridtopic.value: + return EventGridEndpointBuilder( + endpoint_resource_name=endpoint_resource_name, + endpoint_resource_group=endpoint_resource_group, + auth_type=auth_type, + dead_letter_secret=dead_letter_secret, + dead_letter_uri=dead_letter_uri, + endpoint_subscription=endpoint_subscription, + ).build_endpoint() + + if endpoint_resource_type == ADTEndpointType.servicebus.value: + return ServiceBusEndpointBuilder( + endpoint_resource_name=endpoint_resource_name, + endpoint_resource_group=endpoint_resource_group, + auth_type=auth_type, + dead_letter_secret=dead_letter_secret, + dead_letter_uri=dead_letter_uri, + endpoint_subscription=endpoint_subscription, + endpoint_resource_namespace=endpoint_resource_namespace, + endpoint_resource_policy=endpoint_resource_policy, + ).build_endpoint() + + if endpoint_resource_type == ADTEndpointType.eventhub.value: + return EventHubEndpointBuilder( + endpoint_resource_name=endpoint_resource_name, + endpoint_resource_group=endpoint_resource_group, + auth_type=auth_type, + dead_letter_secret=dead_letter_secret, + dead_letter_uri=dead_letter_uri, + endpoint_subscription=endpoint_subscription, + endpoint_resource_namespace=endpoint_resource_namespace, + endpoint_resource_policy=endpoint_resource_policy, + ).build_endpoint() + + raise ValueError("{} not supported.".format(endpoint_resource_type)) diff --git a/azext_iot/digitaltwins/providers/rbac.py b/azext_iot/digitaltwins/providers/rbac.py index 0e2710b65..a449a54f5 100644 --- a/azext_iot/digitaltwins/providers/rbac.py +++ b/azext_iot/digitaltwins/providers/rbac.py @@ -57,3 +57,14 @@ def remove_role(self, dt_scope, assignee, role_type=None): if not delete_op.success(): raise CLIError("Unable to remove role assignment.") return + + def assign_role_flex(self, principal_id, scope, principal_type="ServicePrincipal", role_type="Contributor"): + assign_op = self.cli.invoke( + "role assignment create --scope '{}' --role '{}' --assignee-object-id '{}' --assignee-principal-type '{}' ".format( + scope, role_type, principal_id, principal_type + ) + ) + if not assign_op.success(): + raise CLIError("Unable to assign role.") + + return assign_op.as_json() diff --git a/azext_iot/digitaltwins/providers/resource.py b/azext_iot/digitaltwins/providers/resource.py index 97db61f7b..f643764e7 100644 --- a/azext_iot/digitaltwins/providers/resource.py +++ b/azext_iot/digitaltwins/providers/resource.py @@ -3,7 +3,10 @@ # 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.digitaltwins.common import ( + ADTEndpointAuthType, + ADTPublicNetworkAccessType, +) from azext_iot.digitaltwins.providers import ( DigitalTwinsResourceManager, CloudError, @@ -11,12 +14,13 @@ ) from azext_iot.digitaltwins.providers.rbac import RbacProvider from azext_iot.sdk.digitaltwins.controlplane.models import ( - EventGrid as EventGridEndpointProperties, - EventHub as EventHubEndpointProperties, - ServiceBus as ServiceBusEndpointProperties, + DigitalTwinsDescription, ) from azext_iot.common.utility import unpack_msrest_error from knack.util import CLIError +from knack.log import get_logger + +logger = get_logger(__name__) class ResourceProvider(DigitalTwinsResourceManager): @@ -25,8 +29,18 @@ def __init__(self, cmd): self.mgmt_sdk = self.get_mgmt_sdk() self.rbac = RbacProvider() - def create(self, name, resource_group_name, location=None, tags=None, timeout=60): - + def create( + self, + name, + resource_group_name, + location=None, + tags=None, + timeout=60, + assign_identity=None, + scopes=None, + role_type="Contributor", + public_network_access=ADTPublicNetworkAccessType.enabled.value, + ): if not location: from azext_iot.common.embedded_cli import EmbeddedCLI @@ -38,13 +52,52 @@ def create(self, name, resource_group_name, location=None, tags=None, timeout=60 location = resource_group_meta["location"] try: - return self.mgmt_sdk.digital_twins.create_or_update( - resource_name=name, - resource_group_name=resource_group_name, + if assign_identity: + if scopes and not role_type: + raise CLIError( + "Both --scopes and --role values are required when assigning the instance identity." + ) + + digital_twins_create = DigitalTwinsDescription( location=location, tags=tags, + identity={"type": "SystemAssigned" if assign_identity else "None"}, + public_network_access=public_network_access, + ) + create_or_update = self.mgmt_sdk.digital_twins.create_or_update( + resource_name=name, + resource_group_name=resource_group_name, + digital_twins_create=digital_twins_create, long_running_operation_timeout=timeout, ) + + def rbac_handler(lro): + instance = lro.resource().as_dict() + identity = instance.get("identity") + if identity: + identity_type = identity.get("type") + principal_id = identity.get("principal_id") + + if ( + principal_id + and scopes + and identity_type + and identity_type.lower() == "systemassigned" + ): + for scope in scopes: + logger.info( + "Applying rbac assignment: Principal Id: {}, Scope: {}, Role: {}".format( + principal_id, scope, role_type + ) + ) + self.rbac.assign_role_flex( + principal_id=principal_id, + scope=scope, + role_type=role_type, + ) + + create_or_update.add_done_callback(rbac_handler) + return create_or_update except CloudError as e: raise e except ErrorResponseException as err: @@ -216,7 +269,6 @@ def delete_endpoint(self, name, endpoint_name, resource_group_name=None): except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) - # TODO: Breakout and refactor def add_endpoint( self, name, @@ -227,114 +279,65 @@ def add_endpoint( endpoint_resource_policy=None, endpoint_resource_namespace=None, endpoint_subscription=None, - dead_letter_endpoint=None, - tags=None, + dead_letter_uri=None, + dead_letter_secret=None, resource_group_name=None, timeout=20, + auth_type=None, ): - from azext_iot.common.embedded_cli import EmbeddedCLI from azext_iot.digitaltwins.common import ADTEndpointType - requires_policy = [ADTEndpointType.eventhub, ADTEndpointType.servicebus] - if endpoint_resource_type in requires_policy: - if not endpoint_resource_policy: + requires_namespace = [ + ADTEndpointType.eventhub.value, + ADTEndpointType.servicebus.value, + ] + if endpoint_resource_type in requires_namespace: + if not endpoint_resource_namespace: raise CLIError( - "Endpoint resources of type {} require a policy name.".format( - " or ".join(map(str, requires_policy)) + "Endpoint resources of type {} require a namespace.".format( + " or ".join(map(str, requires_namespace)) ) ) - if not endpoint_resource_namespace: + if ( + auth_type == ADTEndpointAuthType.keybased.value + and not endpoint_resource_policy + ): raise CLIError( - "Endpoint resources of type {} require a namespace.".format( - " or ".join(map(str, requires_policy)) + "Endpoint resources of type {} require a policy name when using Key based integration.".format( + " or ".join(map(str, requires_namespace)) ) ) + if dead_letter_uri and auth_type == ADTEndpointAuthType.keybased.value: + raise CLIError( + "Use --deadletter-sas-uri to support deadletter for a Key based endpoint." + ) + + if dead_letter_secret and auth_type == ADTEndpointAuthType.identitybased.value: + raise CLIError( + "Use --deadletter-uri to support deadletter for an Identity based endpoint." + ) + target_instance = self.find_instance( name=name, resource_group_name=resource_group_name ) if not resource_group_name: resource_group_name = self.get_rg(target_instance) - cli = EmbeddedCLI() - error_prefix = "Could not create ADT instance endpoint. Unable to retrieve" - - properties = {} - - if endpoint_resource_type == ADTEndpointType.eventgridtopic: - eg_topic_keys_op = cli.invoke( - "eventgrid topic key list -n {} -g {}".format( - endpoint_resource_name, endpoint_resource_group - ), - subscription=endpoint_subscription, - ) - if not eg_topic_keys_op.success(): - raise CLIError("{} Event Grid topic keys.".format(error_prefix)) - eg_topic_keys = eg_topic_keys_op.as_json() - - eg_topic_endpoint_op = cli.invoke( - "eventgrid topic show -n {} -g {}".format( - endpoint_resource_name, endpoint_resource_group - ), - subscription=endpoint_subscription, - ) - if not eg_topic_endpoint_op.success(): - raise CLIError("{} Event Grid topic endpoint.".format(error_prefix)) - eg_topic_endpoint = eg_topic_endpoint_op.as_json() - - properties = EventGridEndpointProperties( - access_key1=eg_topic_keys["key1"], - access_key2=eg_topic_keys["key2"], - dead_letter_secret=dead_letter_endpoint, - topic_endpoint=eg_topic_endpoint["endpoint"], - ) - - elif endpoint_resource_type == ADTEndpointType.servicebus: - sb_topic_keys_op = cli.invoke( - "servicebus topic authorization-rule keys list -n {} " - "--namespace-name {} -g {} --topic-name {}".format( - endpoint_resource_policy, - endpoint_resource_namespace, - endpoint_resource_group, - endpoint_resource_name, - ), - subscription=endpoint_subscription, - ) - if not sb_topic_keys_op.success(): - raise CLIError("{} Service Bus topic keys.".format(error_prefix)) - sb_topic_keys = sb_topic_keys_op.as_json() - - properties = ServiceBusEndpointProperties( - primary_connection_string=sb_topic_keys["primaryConnectionString"], - secondary_connection_string=sb_topic_keys["secondaryConnectionString"], - dead_letter_secret=dead_letter_endpoint, - ) - - elif endpoint_resource_type == ADTEndpointType.eventhub: - eventhub_topic_keys_op = cli.invoke( - "eventhubs eventhub authorization-rule keys list -n {} " - "--namespace-name {} -g {} --eventhub-name {}".format( - endpoint_resource_policy, - endpoint_resource_namespace, - endpoint_resource_group, - endpoint_resource_name, - ), - subscription=endpoint_subscription, - ) - if not eventhub_topic_keys_op.success(): - raise CLIError("{} Event Hub keys.".format(error_prefix)) - eventhub_topic_keys = eventhub_topic_keys_op.as_json() - - properties = EventHubEndpointProperties( - connection_string_primary_key=eventhub_topic_keys[ - "primaryConnectionString" - ], - connection_string_secondary_key=eventhub_topic_keys[ - "secondaryConnectionString" - ], - dead_letter_secret=dead_letter_endpoint, - ) + from azext_iot.digitaltwins.providers.endpoint.builders import build_endpoint + + properties = build_endpoint( + endpoint_resource_type=endpoint_resource_type, + endpoint_resource_name=endpoint_resource_name, + endpoint_resource_group=endpoint_resource_group, + endpoint_subscription=endpoint_subscription, + endpoint_resource_namespace=endpoint_resource_namespace, + endpoint_resource_policy=endpoint_resource_policy, + auth_type=auth_type, + dead_letter_secret=dead_letter_secret, + dead_letter_uri=dead_letter_uri, + ) try: return self.mgmt_sdk.digital_twins_endpoint.create_or_update( @@ -346,3 +349,119 @@ def add_endpoint( ) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) + + def get_private_link(self, name, link_name, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + try: + return self.mgmt_sdk.private_link_resources.get( + resource_group_name=resource_group_name, + resource_name=name, + resource_id=link_name, + raw=True, + ).response.json() + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def list_private_links(self, name, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + try: + # This resource is not paged though it may have been the intent. + link_collection = self.mgmt_sdk.private_link_resources.list( + resource_group_name=resource_group_name, resource_name=name, raw=True + ).response.json() + return link_collection.get("value", []) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def set_private_endpoint_conn( + self, + name, + conn_name, + status, + description, + actions_required=None, + group_ids=None, + resource_group_name=None, + ): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + try: + return self.mgmt_sdk.private_endpoint_connections.create_or_update( + resource_group_name=resource_group_name, + resource_name=name, + private_endpoint_connection_name=conn_name, + properties={ + "privateLinkServiceConnectionState": { + "status": status, + "description": description, + "actions_required": actions_required, + }, + "groupIds": group_ids, + }, + ) + + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def get_private_endpoint_conn(self, name, conn_name, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + try: + 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() + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def list_private_endpoint_conns(self, name, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + try: + # This resource is not paged though it may have been the intent. + endpoint_collection = self.mgmt_sdk.private_endpoint_connections.list( + resource_group_name=resource_group_name, resource_name=name, raw=True + ).response.json() + return endpoint_collection.get("value", []) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) + + def delete_private_endpoint_conn(self, name, conn_name, resource_group_name=None): + target_instance = self.find_instance( + name=name, resource_group_name=resource_group_name + ) + if not resource_group_name: + resource_group_name = self.get_rg(target_instance) + + try: + return self.mgmt_sdk.private_endpoint_connections.delete( + resource_group_name=resource_group_name, + resource_name=name, + private_endpoint_connection_name=conn_name + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/digitaltwins/providers/twin.py b/azext_iot/digitaltwins/providers/twin.py index 8e36aa2f3..b9f64c96f 100644 --- a/azext_iot/digitaltwins/providers/twin.py +++ b/azext_iot/digitaltwins/providers/twin.py @@ -20,7 +20,9 @@ class TwinProvider(DigitalTwinsProvider): def __init__(self, cmd, name, rg=None): super(TwinProvider, self).__init__( - cmd=cmd, name=name, rg=rg, + cmd=cmd, + name=name, + rg=rg, ) self.model_provider = ModelProvider(cmd=cmd, name=name, rg=rg) self.query_sdk = self.get_sdk().query diff --git a/azext_iot/sdk/digitaltwins/controlplane/azure_digital_twins_management_client.py b/azext_iot/sdk/digitaltwins/controlplane/azure_digital_twins_management_client.py index 1f6f0e312..03e9b6150 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/azure_digital_twins_management_client.py +++ b/azext_iot/sdk/digitaltwins/controlplane/azure_digital_twins_management_client.py @@ -16,8 +16,9 @@ from .operations.digital_twins_operations import DigitalTwinsOperations from .operations.digital_twins_endpoint_operations import DigitalTwinsEndpointOperations from .operations.operations import Operations +from .operations.private_link_resources_operations import PrivateLinkResourcesOperations +from .operations.private_endpoint_connections_operations import PrivateEndpointConnectionsOperations from . import models -from azext_iot.constants import USER_AGENT class AzureDigitalTwinsManagementClientConfiguration(AzureConfiguration): @@ -46,7 +47,6 @@ def __init__( super(AzureDigitalTwinsManagementClientConfiguration, self).__init__(base_url) self.add_user_agent('azure-mgmt-digitaltwins/{}'.format(VERSION)) - self.add_user_agent(USER_AGENT) self.credentials = credentials self.subscription_id = subscription_id @@ -59,11 +59,15 @@ class AzureDigitalTwinsManagementClient(SDKClient): :vartype config: AzureDigitalTwinsManagementClientConfiguration :ivar digital_twins: DigitalTwins operations - :vartype digital_twins: azure.mgmt.digitaltwins.operations.DigitalTwinsOperations + :vartype digital_twins: controlplane.operations.DigitalTwinsOperations :ivar digital_twins_endpoint: DigitalTwinsEndpoint operations - :vartype digital_twins_endpoint: azure.mgmt.digitaltwins.operations.DigitalTwinsEndpointOperations + :vartype digital_twins_endpoint: controlplane.operations.DigitalTwinsEndpointOperations :ivar operations: Operations operations - :vartype operations: azure.mgmt.digitaltwins.operations.Operations + :vartype operations: controlplane.operations.Operations + :ivar private_link_resources: PrivateLinkResources operations + :vartype private_link_resources: controlplane.operations.PrivateLinkResourcesOperations + :ivar private_endpoint_connections: PrivateEndpointConnections operations + :vartype private_endpoint_connections: controlplane.operations.PrivateEndpointConnectionsOperations :param credentials: Credentials needed for the client to connect to Azure. :type credentials: :mod:`A msrestazure Credentials @@ -80,7 +84,7 @@ def __init__( super(AzureDigitalTwinsManagementClient, self).__init__(self.config.credentials, self.config) client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2020-10-31' + self.api_version = '2020-12-01' self._serialize = Serializer(client_models) self._deserialize = Deserializer(client_models) @@ -90,3 +94,7 @@ def __init__( self._client, self.config, self._serialize, self._deserialize) self.operations = Operations( self._client, self.config, self._serialize, self._deserialize) + self.private_link_resources = PrivateLinkResourcesOperations( + self._client, self.config, self._serialize, self._deserialize) + self.private_endpoint_connections = PrivateEndpointConnectionsOperations( + self._client, self.config, self._serialize, self._deserialize) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/__init__.py b/azext_iot/sdk/digitaltwins/controlplane/models/__init__.py index 92d8cf33c..ed6089378 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/__init__.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/__init__.py @@ -10,7 +10,11 @@ # -------------------------------------------------------------------------- try: + from .digital_twins_patch_properties_py3 import DigitalTwinsPatchProperties + from .private_endpoint_connection_properties_py3 import PrivateEndpointConnectionProperties + from .private_endpoint_connection_py3 import PrivateEndpointConnection from .digital_twins_description_py3 import DigitalTwinsDescription + from .digital_twins_identity_py3 import DigitalTwinsIdentity from .digital_twins_patch_description_py3 import DigitalTwinsPatchDescription from .digital_twins_resource_py3 import DigitalTwinsResource from .error_definition_py3 import ErrorDefinition @@ -25,8 +29,22 @@ from .service_bus_py3 import ServiceBus from .event_hub_py3 import EventHub from .event_grid_py3 import EventGrid + from .group_id_information_properties_py3 import GroupIdInformationProperties + from .group_id_information_properties_model_py3 import GroupIdInformationPropertiesModel + from .group_id_information_py3 import GroupIdInformation + from .private_endpoint_connections_response_py3 import PrivateEndpointConnectionsResponse + from .group_id_information_response_py3 import GroupIdInformationResponse + from .connection_state_py3 import ConnectionState + from .private_endpoint_py3 import PrivateEndpoint + from .connection_properties_private_endpoint_py3 import ConnectionPropertiesPrivateEndpoint + from .connection_properties_private_link_service_connection_state_py3 import ConnectionPropertiesPrivateLinkServiceConnectionState + from .connection_properties_py3 import ConnectionProperties except (SyntaxError, ImportError): + from .digital_twins_patch_properties import DigitalTwinsPatchProperties + from .private_endpoint_connection_properties import PrivateEndpointConnectionProperties + from .private_endpoint_connection import PrivateEndpointConnection from .digital_twins_description import DigitalTwinsDescription + from .digital_twins_identity import DigitalTwinsIdentity from .digital_twins_patch_description import DigitalTwinsPatchDescription from .digital_twins_resource import DigitalTwinsResource from .error_definition import ErrorDefinition @@ -41,17 +59,36 @@ from .service_bus import ServiceBus from .event_hub import EventHub from .event_grid import EventGrid + from .group_id_information_properties import GroupIdInformationProperties + from .group_id_information_properties_model import GroupIdInformationPropertiesModel + from .group_id_information import GroupIdInformation + from .private_endpoint_connections_response import PrivateEndpointConnectionsResponse + from .group_id_information_response import GroupIdInformationResponse + from .connection_state import ConnectionState + from .private_endpoint import PrivateEndpoint + from .connection_properties_private_endpoint import ConnectionPropertiesPrivateEndpoint + from .connection_properties_private_link_service_connection_state import ConnectionPropertiesPrivateLinkServiceConnectionState + from .connection_properties import ConnectionProperties from .digital_twins_description_paged import DigitalTwinsDescriptionPaged from .digital_twins_endpoint_resource_paged import DigitalTwinsEndpointResourcePaged from .operation_paged import OperationPaged from .azure_digital_twins_management_client_enums import ( + PublicNetworkAccess, ProvisioningState, + DigitalTwinsIdentityType, Reason, EndpointProvisioningState, + AuthenticationType, + PrivateLinkServiceConnectionStatus, + ConnectionPropertiesProvisioningState, ) __all__ = [ + 'DigitalTwinsPatchProperties', + 'PrivateEndpointConnectionProperties', + 'PrivateEndpointConnection', 'DigitalTwinsDescription', + 'DigitalTwinsIdentity', 'DigitalTwinsPatchDescription', 'DigitalTwinsResource', 'ErrorDefinition', @@ -66,10 +103,25 @@ 'ServiceBus', 'EventHub', 'EventGrid', + 'GroupIdInformationProperties', + 'GroupIdInformationPropertiesModel', + 'GroupIdInformation', + 'PrivateEndpointConnectionsResponse', + 'GroupIdInformationResponse', + 'ConnectionState', + 'PrivateEndpoint', + 'ConnectionPropertiesPrivateEndpoint', + 'ConnectionPropertiesPrivateLinkServiceConnectionState', + 'ConnectionProperties', 'DigitalTwinsDescriptionPaged', 'DigitalTwinsEndpointResourcePaged', 'OperationPaged', + 'PublicNetworkAccess', 'ProvisioningState', + 'DigitalTwinsIdentityType', 'Reason', 'EndpointProvisioningState', + 'AuthenticationType', + 'PrivateLinkServiceConnectionStatus', + 'ConnectionPropertiesProvisioningState', ] diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/azure_digital_twins_management_client_enums.py b/azext_iot/sdk/digitaltwins/controlplane/models/azure_digital_twins_management_client_enums.py index ff6d2b607..c5cef2830 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/azure_digital_twins_management_client_enums.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/azure_digital_twins_management_client_enums.py @@ -12,10 +12,17 @@ from enum import Enum +class PublicNetworkAccess(str, Enum): + + enabled = "Enabled" + disabled = "Disabled" + + class ProvisioningState(str, Enum): provisioning = "Provisioning" deleting = "Deleting" + updating = "Updating" succeeded = "Succeeded" failed = "Failed" canceled = "Canceled" @@ -26,6 +33,12 @@ class ProvisioningState(str, Enum): moving = "Moving" +class DigitalTwinsIdentityType(str, Enum): + + none = "None" + system_assigned = "SystemAssigned" + + class Reason(str, Enum): invalid = "Invalid" @@ -45,3 +58,25 @@ class EndpointProvisioningState(str, Enum): restoring = "Restoring" moving = "Moving" disabled = "Disabled" + + +class AuthenticationType(str, Enum): + + key_based = "KeyBased" + identity_based = "IdentityBased" + + +class PrivateLinkServiceConnectionStatus(str, Enum): + + pending = "Pending" + approved = "Approved" + rejected = "Rejected" + disconnected = "Disconnected" + + +class ConnectionPropertiesProvisioningState(str, Enum): + + pending = "Pending" + approved = "Approved" + rejected = "Rejected" + disconnected = "Disconnected" diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result.py b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result.py index 5fddb0bfe..bd2213e80 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result.py @@ -23,7 +23,7 @@ class CheckNameResult(Model): :type message: str :param reason: Message providing the reason why the given name is invalid. Possible values include: 'Invalid', 'AlreadyExists' - :type reason: str or ~azure.mgmt.digitaltwins.models.Reason + :type reason: str or ~controlplane.models.Reason """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result_py3.py index 7c2daedbd..76d83ffcf 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/check_name_result_py3.py @@ -23,7 +23,7 @@ class CheckNameResult(Model): :type message: str :param reason: Message providing the reason why the given name is invalid. Possible values include: 'Invalid', 'AlreadyExists' - :type reason: str or ~azure.mgmt.digitaltwins.models.Reason + :type reason: str or ~controlplane.models.Reason """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties.py b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties.py new file mode 100644 index 000000000..0fb707cf0 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ConnectionProperties(Model): + """The properties of a private endpoint connection. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Pending', 'Approved', 'Rejected', 'Disconnected' + :vartype provisioning_state: str or + ~controlplane.models.ConnectionPropertiesProvisioningState + :param private_endpoint: + :type private_endpoint: + ~controlplane.models.ConnectionPropertiesPrivateEndpoint + :param group_ids: The list of group ids for the private endpoint + connection. + :type group_ids: list[str] + :param private_link_service_connection_state: + :type private_link_service_connection_state: + ~controlplane.models.ConnectionPropertiesPrivateLinkServiceConnectionState + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'private_endpoint': {'key': 'privateEndpoint', 'type': 'ConnectionPropertiesPrivateEndpoint'}, + 'group_ids': {'key': 'groupIds', 'type': '[str]'}, + 'private_link_service_connection_state': {'key': 'privateLinkServiceConnectionState', 'type': 'ConnectionPropertiesPrivateLinkServiceConnectionState'}, + } + + def __init__(self, **kwargs): + super(ConnectionProperties, self).__init__(**kwargs) + self.provisioning_state = None + self.private_endpoint = kwargs.get('private_endpoint', None) + self.group_ids = kwargs.get('group_ids', None) + self.private_link_service_connection_state = kwargs.get('private_link_service_connection_state', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_endpoint.py b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_endpoint.py new file mode 100644 index 000000000..3522e5561 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_endpoint.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .private_endpoint import PrivateEndpoint + + +class ConnectionPropertiesPrivateEndpoint(PrivateEndpoint): + """ConnectionPropertiesPrivateEndpoint. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + """ + + _validation = { + 'id': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ConnectionPropertiesPrivateEndpoint, self).__init__(**kwargs) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_endpoint_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_endpoint_py3.py new file mode 100644 index 000000000..b3d1e2b29 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_endpoint_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .private_endpoint_py3 import PrivateEndpoint + + +class ConnectionPropertiesPrivateEndpoint(PrivateEndpoint): + """ConnectionPropertiesPrivateEndpoint. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + """ + + _validation = { + 'id': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + } + + def __init__(self, **kwargs) -> None: + super(ConnectionPropertiesPrivateEndpoint, self).__init__(**kwargs) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_link_service_connection_state.py b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_link_service_connection_state.py new file mode 100644 index 000000000..1d6a9870b --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_link_service_connection_state.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .connection_state import ConnectionState + + +class ConnectionPropertiesPrivateLinkServiceConnectionState(ConnectionState): + """ConnectionPropertiesPrivateLinkServiceConnectionState. + + All required parameters must be populated in order to send to Azure. + + :param status: Required. The status of a private endpoint connection. + Possible values include: 'Pending', 'Approved', 'Rejected', 'Disconnected' + :type status: str or + ~controlplane.models.PrivateLinkServiceConnectionStatus + :param description: Required. The description for the current state of a + private endpoint connection. + :type description: str + :param actions_required: Actions required for a private endpoint + connection. + :type actions_required: str + """ + + _validation = { + 'status': {'required': True}, + 'description': {'required': True}, + } + + _attribute_map = { + 'status': {'key': 'status', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + 'actions_required': {'key': 'actionsRequired', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ConnectionPropertiesPrivateLinkServiceConnectionState, self).__init__(**kwargs) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_link_service_connection_state_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_link_service_connection_state_py3.py new file mode 100644 index 000000000..6e5964952 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_private_link_service_connection_state_py3.py @@ -0,0 +1,44 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .connection_state_py3 import ConnectionState + + +class ConnectionPropertiesPrivateLinkServiceConnectionState(ConnectionState): + """ConnectionPropertiesPrivateLinkServiceConnectionState. + + All required parameters must be populated in order to send to Azure. + + :param status: Required. The status of a private endpoint connection. + Possible values include: 'Pending', 'Approved', 'Rejected', 'Disconnected' + :type status: str or + ~controlplane.models.PrivateLinkServiceConnectionStatus + :param description: Required. The description for the current state of a + private endpoint connection. + :type description: str + :param actions_required: Actions required for a private endpoint + connection. + :type actions_required: str + """ + + _validation = { + 'status': {'required': True}, + 'description': {'required': True}, + } + + _attribute_map = { + 'status': {'key': 'status', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + 'actions_required': {'key': 'actionsRequired', 'type': 'str'}, + } + + def __init__(self, *, status, description: str, actions_required: str=None, **kwargs) -> None: + super(ConnectionPropertiesPrivateLinkServiceConnectionState, self).__init__(status=status, description=description, actions_required=actions_required, **kwargs) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_py3.py new file mode 100644 index 000000000..25ef458e9 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/connection_properties_py3.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ConnectionProperties(Model): + """The properties of a private endpoint connection. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Pending', 'Approved', 'Rejected', 'Disconnected' + :vartype provisioning_state: str or + ~controlplane.models.ConnectionPropertiesProvisioningState + :param private_endpoint: + :type private_endpoint: + ~controlplane.models.ConnectionPropertiesPrivateEndpoint + :param group_ids: The list of group ids for the private endpoint + connection. + :type group_ids: list[str] + :param private_link_service_connection_state: + :type private_link_service_connection_state: + ~controlplane.models.ConnectionPropertiesPrivateLinkServiceConnectionState + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'private_endpoint': {'key': 'privateEndpoint', 'type': 'ConnectionPropertiesPrivateEndpoint'}, + 'group_ids': {'key': 'groupIds', 'type': '[str]'}, + 'private_link_service_connection_state': {'key': 'privateLinkServiceConnectionState', 'type': 'ConnectionPropertiesPrivateLinkServiceConnectionState'}, + } + + def __init__(self, *, private_endpoint=None, group_ids=None, private_link_service_connection_state=None, **kwargs) -> None: + super(ConnectionProperties, self).__init__(**kwargs) + self.provisioning_state = None + self.private_endpoint = private_endpoint + self.group_ids = group_ids + self.private_link_service_connection_state = private_link_service_connection_state diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/connection_state.py b/azext_iot/sdk/digitaltwins/controlplane/models/connection_state.py new file mode 100644 index 000000000..01ecfdc8a --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/connection_state.py @@ -0,0 +1,47 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ConnectionState(Model): + """The current state of a private endpoint connection. + + All required parameters must be populated in order to send to Azure. + + :param status: Required. The status of a private endpoint connection. + Possible values include: 'Pending', 'Approved', 'Rejected', 'Disconnected' + :type status: str or + ~controlplane.models.PrivateLinkServiceConnectionStatus + :param description: Required. The description for the current state of a + private endpoint connection. + :type description: str + :param actions_required: Actions required for a private endpoint + connection. + :type actions_required: str + """ + + _validation = { + 'status': {'required': True}, + 'description': {'required': True}, + } + + _attribute_map = { + 'status': {'key': 'status', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + 'actions_required': {'key': 'actionsRequired', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ConnectionState, self).__init__(**kwargs) + self.status = kwargs.get('status', None) + self.description = kwargs.get('description', None) + self.actions_required = kwargs.get('actions_required', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/connection_state_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/connection_state_py3.py new file mode 100644 index 000000000..938552448 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/connection_state_py3.py @@ -0,0 +1,47 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class ConnectionState(Model): + """The current state of a private endpoint connection. + + All required parameters must be populated in order to send to Azure. + + :param status: Required. The status of a private endpoint connection. + Possible values include: 'Pending', 'Approved', 'Rejected', 'Disconnected' + :type status: str or + ~controlplane.models.PrivateLinkServiceConnectionStatus + :param description: Required. The description for the current state of a + private endpoint connection. + :type description: str + :param actions_required: Actions required for a private endpoint + connection. + :type actions_required: str + """ + + _validation = { + 'status': {'required': True}, + 'description': {'required': True}, + } + + _attribute_map = { + 'status': {'key': 'status', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + 'actions_required': {'key': 'actionsRequired', 'type': 'str'}, + } + + def __init__(self, *, status, description: str, actions_required: str=None, **kwargs) -> None: + super(ConnectionState, self).__init__(**kwargs) + self.status = status + self.description = description + self.actions_required = actions_required diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description.py index 273c7e339..22e7d559b 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description.py @@ -30,17 +30,25 @@ class DigitalTwinsDescription(DigitalTwinsResource): :type location: str :param tags: The resource tags. :type tags: dict[str, str] + :param identity: The managed identity for the DigitalTwinsInstance. + :type identity: ~controlplane.models.DigitalTwinsIdentity :ivar created_time: Time when DigitalTwinsInstance was created. :vartype created_time: datetime :ivar last_updated_time: Time when DigitalTwinsInstance was updated. :vartype last_updated_time: datetime :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', - 'Warning', 'Suspending', 'Restoring', 'Moving' - :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.ProvisioningState + 'Provisioning', 'Deleting', 'Updating', 'Succeeded', 'Failed', 'Canceled', + 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving' + :vartype provisioning_state: str or ~controlplane.models.ProvisioningState :ivar host_name: Api endpoint to work with DigitalTwinsInstance. :vartype host_name: str + :param private_endpoint_connections: + :type private_endpoint_connections: + list[~controlplane.models.PrivateEndpointConnection] + :param public_network_access: Public network access for the + DigitalTwinsInstance. Possible values include: 'Enabled', 'Disabled' + :type public_network_access: str or + ~controlplane.models.PublicNetworkAccess """ _validation = { @@ -60,10 +68,13 @@ class DigitalTwinsDescription(DigitalTwinsResource): 'type': {'key': 'type', 'type': 'str'}, 'location': {'key': 'location', 'type': 'str'}, 'tags': {'key': 'tags', 'type': '{str}'}, + 'identity': {'key': 'identity', 'type': 'DigitalTwinsIdentity'}, 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, 'last_updated_time': {'key': 'properties.lastUpdatedTime', 'type': 'iso-8601'}, 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, 'host_name': {'key': 'properties.hostName', 'type': 'str'}, + 'private_endpoint_connections': {'key': 'properties.privateEndpointConnections', 'type': '[PrivateEndpointConnection]'}, + 'public_network_access': {'key': 'properties.publicNetworkAccess', 'type': 'str'}, } def __init__(self, **kwargs): @@ -72,3 +83,5 @@ def __init__(self, **kwargs): self.last_updated_time = None self.provisioning_state = None self.host_name = None + self.private_endpoint_connections = kwargs.get('private_endpoint_connections', None) + self.public_network_access = kwargs.get('public_network_access', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_paged.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_paged.py index c33257836..be83f1c8a 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_paged.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_paged.py @@ -14,7 +14,7 @@ class DigitalTwinsDescriptionPaged(Paged): """ - A paging container for iterating over a list of :class:`DigitalTwinsDescription ` object + A paging container for iterating over a list of :class:`DigitalTwinsDescription ` object """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_py3.py index 648525f75..baeaccee7 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_description_py3.py @@ -30,17 +30,25 @@ class DigitalTwinsDescription(DigitalTwinsResource): :type location: str :param tags: The resource tags. :type tags: dict[str, str] + :param identity: The managed identity for the DigitalTwinsInstance. + :type identity: ~controlplane.models.DigitalTwinsIdentity :ivar created_time: Time when DigitalTwinsInstance was created. :vartype created_time: datetime :ivar last_updated_time: Time when DigitalTwinsInstance was updated. :vartype last_updated_time: datetime :ivar provisioning_state: The provisioning state. Possible values include: - 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', - 'Warning', 'Suspending', 'Restoring', 'Moving' - :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.ProvisioningState + 'Provisioning', 'Deleting', 'Updating', 'Succeeded', 'Failed', 'Canceled', + 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving' + :vartype provisioning_state: str or ~controlplane.models.ProvisioningState :ivar host_name: Api endpoint to work with DigitalTwinsInstance. :vartype host_name: str + :param private_endpoint_connections: + :type private_endpoint_connections: + list[~controlplane.models.PrivateEndpointConnection] + :param public_network_access: Public network access for the + DigitalTwinsInstance. Possible values include: 'Enabled', 'Disabled' + :type public_network_access: str or + ~controlplane.models.PublicNetworkAccess """ _validation = { @@ -60,15 +68,20 @@ class DigitalTwinsDescription(DigitalTwinsResource): 'type': {'key': 'type', 'type': 'str'}, 'location': {'key': 'location', 'type': 'str'}, 'tags': {'key': 'tags', 'type': '{str}'}, + 'identity': {'key': 'identity', 'type': 'DigitalTwinsIdentity'}, 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, 'last_updated_time': {'key': 'properties.lastUpdatedTime', 'type': 'iso-8601'}, 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, 'host_name': {'key': 'properties.hostName', 'type': 'str'}, + 'private_endpoint_connections': {'key': 'properties.privateEndpointConnections', 'type': '[PrivateEndpointConnection]'}, + 'public_network_access': {'key': 'properties.publicNetworkAccess', 'type': 'str'}, } - def __init__(self, *, location: str, tags=None, **kwargs) -> None: - super(DigitalTwinsDescription, self).__init__(location=location, tags=tags, **kwargs) + def __init__(self, *, location: str, tags=None, identity=None, private_endpoint_connections=None, public_network_access=None, **kwargs) -> None: + super(DigitalTwinsDescription, self).__init__(location=location, tags=tags, identity=identity, **kwargs) self.created_time = None self.last_updated_time = None self.provisioning_state = None self.host_name = None + self.private_endpoint_connections = private_endpoint_connections + self.public_network_access = public_network_access diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource.py index 54b6809db..77f8d379d 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource.py @@ -29,7 +29,7 @@ class DigitalTwinsEndpointResource(ExternalResource): :param properties: Required. DigitalTwinsInstance endpoint resource properties. :type properties: - ~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResourceProperties + ~controlplane.models.DigitalTwinsEndpointResourceProperties """ _validation = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_paged.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_paged.py index 5673e29c2..c66d5f8c1 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_paged.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_paged.py @@ -14,7 +14,7 @@ class DigitalTwinsEndpointResourcePaged(Paged): """ - A paging container for iterating over a list of :class:`DigitalTwinsEndpointResource ` object + A paging container for iterating over a list of :class:`DigitalTwinsEndpointResource ` object """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties.py index 1db0bbf80..f9bd90a60 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties.py @@ -27,13 +27,20 @@ class DigitalTwinsEndpointResourceProperties(Model): 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.EndpointProvisioningState + ~controlplane.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param dead_letter_secret: Dead letter storage secret. Will be obfuscated - during read. + :param authentication_type: Specifies the authentication type being used + for connecting to the endpoint. Possible values include: 'KeyBased', + 'IdentityBased' + :type authentication_type: str or ~controlplane.models.AuthenticationType + :param dead_letter_secret: Dead letter storage secret for key-based + authentication. Will be obfuscated during read. :type dead_letter_secret: str + :param dead_letter_uri: Dead letter storage URL for identity-based + authentication. + :type dead_letter_uri: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str """ @@ -47,7 +54,9 @@ class DigitalTwinsEndpointResourceProperties(Model): _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'authentication_type': {'key': 'authenticationType', 'type': 'str'}, 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, + 'dead_letter_uri': {'key': 'deadLetterUri', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, } @@ -59,5 +68,7 @@ def __init__(self, **kwargs): super(DigitalTwinsEndpointResourceProperties, self).__init__(**kwargs) self.provisioning_state = None self.created_time = None + self.authentication_type = kwargs.get('authentication_type', None) self.dead_letter_secret = kwargs.get('dead_letter_secret', None) + self.dead_letter_uri = kwargs.get('dead_letter_uri', None) self.endpoint_type = None diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties_py3.py index ef101f571..19a172fc1 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_properties_py3.py @@ -27,13 +27,20 @@ class DigitalTwinsEndpointResourceProperties(Model): 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.EndpointProvisioningState + ~controlplane.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param dead_letter_secret: Dead letter storage secret. Will be obfuscated - during read. + :param authentication_type: Specifies the authentication type being used + for connecting to the endpoint. Possible values include: 'KeyBased', + 'IdentityBased' + :type authentication_type: str or ~controlplane.models.AuthenticationType + :param dead_letter_secret: Dead letter storage secret for key-based + authentication. Will be obfuscated during read. :type dead_letter_secret: str + :param dead_letter_uri: Dead letter storage URL for identity-based + authentication. + :type dead_letter_uri: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str """ @@ -47,7 +54,9 @@ class DigitalTwinsEndpointResourceProperties(Model): _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'authentication_type': {'key': 'authenticationType', 'type': 'str'}, 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, + 'dead_letter_uri': {'key': 'deadLetterUri', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, } @@ -55,9 +64,11 @@ class DigitalTwinsEndpointResourceProperties(Model): 'endpoint_type': {'ServiceBus': 'ServiceBus', 'EventHub': 'EventHub', 'EventGrid': 'EventGrid'} } - def __init__(self, *, dead_letter_secret: str=None, **kwargs) -> None: + def __init__(self, *, authentication_type=None, dead_letter_secret: str=None, dead_letter_uri: str=None, **kwargs) -> None: super(DigitalTwinsEndpointResourceProperties, self).__init__(**kwargs) self.provisioning_state = None self.created_time = None + self.authentication_type = authentication_type self.dead_letter_secret = dead_letter_secret + self.dead_letter_uri = dead_letter_uri self.endpoint_type = None diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_py3.py index 79def4503..bc3189328 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_endpoint_resource_py3.py @@ -29,7 +29,7 @@ class DigitalTwinsEndpointResource(ExternalResource): :param properties: Required. DigitalTwinsInstance endpoint resource properties. :type properties: - ~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResourceProperties + ~controlplane.models.DigitalTwinsEndpointResourceProperties """ _validation = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_identity.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_identity.py new file mode 100644 index 000000000..a7560f9cc --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_identity.py @@ -0,0 +1,50 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsIdentity(Model): + """The managed identity for the DigitalTwinsInstance. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param type: The type of Managed Identity used by the + DigitalTwinsInstance. Only SystemAssigned is supported. Possible values + include: 'None', 'SystemAssigned' + :type type: str or ~controlplane.models.DigitalTwinsIdentityType + :ivar principal_id: The object id of the Managed Identity Resource. This + will be sent to the RP from ARM via the x-ms-identity-principal-id header + in the PUT request if the resource has a systemAssigned(implicit) identity + :vartype principal_id: str + :ivar tenant_id: The tenant id of the Managed Identity Resource. This will + be sent to the RP from ARM via the x-ms-client-tenant-id header in the PUT + request if the resource has a systemAssigned(implicit) identity + :vartype tenant_id: str + """ + + _validation = { + 'principal_id': {'readonly': True}, + 'tenant_id': {'readonly': True}, + } + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'principal_id': {'key': 'principalId', 'type': 'str'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsIdentity, self).__init__(**kwargs) + self.type = kwargs.get('type', None) + self.principal_id = None + self.tenant_id = None diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_identity_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_identity_py3.py new file mode 100644 index 000000000..e2c7b19d6 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_identity_py3.py @@ -0,0 +1,50 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsIdentity(Model): + """The managed identity for the DigitalTwinsInstance. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param type: The type of Managed Identity used by the + DigitalTwinsInstance. Only SystemAssigned is supported. Possible values + include: 'None', 'SystemAssigned' + :type type: str or ~controlplane.models.DigitalTwinsIdentityType + :ivar principal_id: The object id of the Managed Identity Resource. This + will be sent to the RP from ARM via the x-ms-identity-principal-id header + in the PUT request if the resource has a systemAssigned(implicit) identity + :vartype principal_id: str + :ivar tenant_id: The tenant id of the Managed Identity Resource. This will + be sent to the RP from ARM via the x-ms-client-tenant-id header in the PUT + request if the resource has a systemAssigned(implicit) identity + :vartype tenant_id: str + """ + + _validation = { + 'principal_id': {'readonly': True}, + 'tenant_id': {'readonly': True}, + } + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'principal_id': {'key': 'principalId', 'type': 'str'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + } + + def __init__(self, *, type=None, **kwargs) -> None: + super(DigitalTwinsIdentity, self).__init__(**kwargs) + self.type = type + self.principal_id = None + self.tenant_id = None diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description.py index a9299bfa4..45eca2eab 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description.py @@ -15,14 +15,22 @@ class DigitalTwinsPatchDescription(Model): """The description of the DigitalTwins service. - :param tags: Instance tags + :param tags: Instance patch properties :type tags: dict[str, str] + :param identity: The managed identity for the DigitalTwinsInstance. + :type identity: ~controlplane.models.DigitalTwinsIdentity + :param properties: Properties for the DigitalTwinsInstance. + :type properties: ~controlplane.models.DigitalTwinsPatchProperties """ _attribute_map = { 'tags': {'key': 'tags', 'type': '{str}'}, + 'identity': {'key': 'identity', 'type': 'DigitalTwinsIdentity'}, + 'properties': {'key': 'properties', 'type': 'DigitalTwinsPatchProperties'}, } def __init__(self, **kwargs): super(DigitalTwinsPatchDescription, self).__init__(**kwargs) self.tags = kwargs.get('tags', None) + self.identity = kwargs.get('identity', None) + self.properties = kwargs.get('properties', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description_py3.py index a8e52a902..a4921909e 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_description_py3.py @@ -15,14 +15,22 @@ class DigitalTwinsPatchDescription(Model): """The description of the DigitalTwins service. - :param tags: Instance tags + :param tags: Instance patch properties :type tags: dict[str, str] + :param identity: The managed identity for the DigitalTwinsInstance. + :type identity: ~controlplane.models.DigitalTwinsIdentity + :param properties: Properties for the DigitalTwinsInstance. + :type properties: ~controlplane.models.DigitalTwinsPatchProperties """ _attribute_map = { 'tags': {'key': 'tags', 'type': '{str}'}, + 'identity': {'key': 'identity', 'type': 'DigitalTwinsIdentity'}, + 'properties': {'key': 'properties', 'type': 'DigitalTwinsPatchProperties'}, } - def __init__(self, *, tags=None, **kwargs) -> None: + def __init__(self, *, tags=None, identity=None, properties=None, **kwargs) -> None: super(DigitalTwinsPatchDescription, self).__init__(**kwargs) self.tags = tags + self.identity = identity + self.properties = properties diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_properties.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_properties.py new file mode 100644 index 000000000..9bee963ae --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_properties.py @@ -0,0 +1,30 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsPatchProperties(Model): + """The properties of a DigitalTwinsInstance. + + :param public_network_access: Public network access for the + DigitalTwinsInstance. Possible values include: 'Enabled', 'Disabled' + :type public_network_access: str or + ~controlplane.models.PublicNetworkAccess + """ + + _attribute_map = { + 'public_network_access': {'key': 'publicNetworkAccess', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DigitalTwinsPatchProperties, self).__init__(**kwargs) + self.public_network_access = kwargs.get('public_network_access', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_properties_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_properties_py3.py new file mode 100644 index 000000000..3e356513b --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_patch_properties_py3.py @@ -0,0 +1,30 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class DigitalTwinsPatchProperties(Model): + """The properties of a DigitalTwinsInstance. + + :param public_network_access: Public network access for the + DigitalTwinsInstance. Possible values include: 'Enabled', 'Disabled' + :type public_network_access: str or + ~controlplane.models.PublicNetworkAccess + """ + + _attribute_map = { + 'public_network_access': {'key': 'publicNetworkAccess', 'type': 'str'}, + } + + def __init__(self, *, public_network_access=None, **kwargs) -> None: + super(DigitalTwinsPatchProperties, self).__init__(**kwargs) + self.public_network_access = public_network_access diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource.py index f5b632031..08674430c 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource.py @@ -30,6 +30,8 @@ class DigitalTwinsResource(Model): :type location: str :param tags: The resource tags. :type tags: dict[str, str] + :param identity: The managed identity for the DigitalTwinsInstance. + :type identity: ~controlplane.models.DigitalTwinsIdentity """ _validation = { @@ -45,6 +47,7 @@ class DigitalTwinsResource(Model): 'type': {'key': 'type', 'type': 'str'}, 'location': {'key': 'location', 'type': 'str'}, 'tags': {'key': 'tags', 'type': '{str}'}, + 'identity': {'key': 'identity', 'type': 'DigitalTwinsIdentity'}, } def __init__(self, **kwargs): @@ -54,3 +57,4 @@ def __init__(self, **kwargs): self.type = None self.location = kwargs.get('location', None) self.tags = kwargs.get('tags', None) + self.identity = kwargs.get('identity', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource_py3.py index b3ea72049..a1c4b6051 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/digital_twins_resource_py3.py @@ -30,6 +30,8 @@ class DigitalTwinsResource(Model): :type location: str :param tags: The resource tags. :type tags: dict[str, str] + :param identity: The managed identity for the DigitalTwinsInstance. + :type identity: ~controlplane.models.DigitalTwinsIdentity """ _validation = { @@ -45,12 +47,14 @@ class DigitalTwinsResource(Model): 'type': {'key': 'type', 'type': 'str'}, 'location': {'key': 'location', 'type': 'str'}, 'tags': {'key': 'tags', 'type': '{str}'}, + 'identity': {'key': 'identity', 'type': 'DigitalTwinsIdentity'}, } - def __init__(self, *, location: str, tags=None, **kwargs) -> None: + def __init__(self, *, location: str, tags=None, identity=None, **kwargs) -> None: super(DigitalTwinsResource, self).__init__(**kwargs) self.id = None self.name = None self.type = None self.location = location self.tags = tags + self.identity = identity diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/error_definition.py b/azext_iot/sdk/digitaltwins/controlplane/models/error_definition.py index c96f086b7..161802641 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/error_definition.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/error_definition.py @@ -24,7 +24,7 @@ class ErrorDefinition(Model): :ivar message: Description of the error. :vartype message: str :ivar details: Internal error details. - :vartype details: list[~azure.mgmt.digitaltwins.models.ErrorDefinition] + :vartype details: list[~controlplane.models.ErrorDefinition] """ _validation = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/error_definition_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/error_definition_py3.py index 8bb60ac87..09504ef69 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/error_definition_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/error_definition_py3.py @@ -24,7 +24,7 @@ class ErrorDefinition(Model): :ivar message: Description of the error. :vartype message: str :ivar details: Internal error details. - :vartype details: list[~azure.mgmt.digitaltwins.models.ErrorDefinition] + :vartype details: list[~controlplane.models.ErrorDefinition] """ _validation = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/error_response.py b/azext_iot/sdk/digitaltwins/controlplane/models/error_response.py index ea865540f..ddc297c39 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/error_response.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/error_response.py @@ -17,7 +17,7 @@ class ErrorResponse(Model): """Error response. :param error: Error description - :type error: ~azure.mgmt.digitaltwins.models.ErrorDefinition + :type error: ~controlplane.models.ErrorDefinition """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/error_response_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/error_response_py3.py index fef3c3115..2d9a3da48 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/error_response_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/error_response_py3.py @@ -17,7 +17,7 @@ class ErrorResponse(Model): """Error response. :param error: Error description - :type error: ~azure.mgmt.digitaltwins.models.ErrorDefinition + :type error: ~controlplane.models.ErrorDefinition """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/event_grid.py b/azext_iot/sdk/digitaltwins/controlplane/models/event_grid.py index 37bfbf98a..c2e176bed 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/event_grid.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/event_grid.py @@ -24,13 +24,20 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.EndpointProvisioningState + ~controlplane.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param dead_letter_secret: Dead letter storage secret. Will be obfuscated - during read. + :param authentication_type: Specifies the authentication type being used + for connecting to the endpoint. Possible values include: 'KeyBased', + 'IdentityBased' + :type authentication_type: str or ~controlplane.models.AuthenticationType + :param dead_letter_secret: Dead letter storage secret for key-based + authentication. Will be obfuscated during read. :type dead_letter_secret: str + :param dead_letter_uri: Dead letter storage URL for identity-based + authentication. + :type dead_letter_uri: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str :param topic_endpoint: Required. EventGrid Topic Endpoint @@ -54,7 +61,9 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'authentication_type': {'key': 'authenticationType', 'type': 'str'}, 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, + 'dead_letter_uri': {'key': 'deadLetterUri', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'topic_endpoint': {'key': 'TopicEndpoint', 'type': 'str'}, 'access_key1': {'key': 'accessKey1', 'type': 'str'}, diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/event_grid_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/event_grid_py3.py index 22674c4b3..5643ed6ed 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/event_grid_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/event_grid_py3.py @@ -24,13 +24,20 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.EndpointProvisioningState + ~controlplane.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param dead_letter_secret: Dead letter storage secret. Will be obfuscated - during read. + :param authentication_type: Specifies the authentication type being used + for connecting to the endpoint. Possible values include: 'KeyBased', + 'IdentityBased' + :type authentication_type: str or ~controlplane.models.AuthenticationType + :param dead_letter_secret: Dead letter storage secret for key-based + authentication. Will be obfuscated during read. :type dead_letter_secret: str + :param dead_letter_uri: Dead letter storage URL for identity-based + authentication. + :type dead_letter_uri: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str :param topic_endpoint: Required. EventGrid Topic Endpoint @@ -54,15 +61,17 @@ class EventGrid(DigitalTwinsEndpointResourceProperties): _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'authentication_type': {'key': 'authenticationType', 'type': 'str'}, 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, + 'dead_letter_uri': {'key': 'deadLetterUri', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'topic_endpoint': {'key': 'TopicEndpoint', 'type': 'str'}, 'access_key1': {'key': 'accessKey1', 'type': 'str'}, 'access_key2': {'key': 'accessKey2', 'type': 'str'}, } - def __init__(self, *, topic_endpoint: str, access_key1: str, dead_letter_secret: str=None, access_key2: str=None, **kwargs) -> None: - super(EventGrid, self).__init__(dead_letter_secret=dead_letter_secret, **kwargs) + def __init__(self, *, topic_endpoint: str, access_key1: str, authentication_type=None, dead_letter_secret: str=None, dead_letter_uri: str=None, access_key2: str=None, **kwargs) -> None: + super(EventGrid, self).__init__(authentication_type=authentication_type, dead_letter_secret=dead_letter_secret, dead_letter_uri=dead_letter_uri, **kwargs) self.topic_endpoint = topic_endpoint self.access_key1 = access_key1 self.access_key2 = access_key2 diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/event_hub.py b/azext_iot/sdk/digitaltwins/controlplane/models/event_hub.py index a3c70304c..2bb4e0584 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/event_hub.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/event_hub.py @@ -24,41 +24,59 @@ class EventHub(DigitalTwinsEndpointResourceProperties): 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.EndpointProvisioningState + ~controlplane.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param dead_letter_secret: Dead letter storage secret. Will be obfuscated - during read. + :param authentication_type: Specifies the authentication type being used + for connecting to the endpoint. Possible values include: 'KeyBased', + 'IdentityBased' + :type authentication_type: str or ~controlplane.models.AuthenticationType + :param dead_letter_secret: Dead letter storage secret for key-based + authentication. Will be obfuscated during read. :type dead_letter_secret: str + :param dead_letter_uri: Dead letter storage URL for identity-based + authentication. + :type dead_letter_uri: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str - :param connection_string_primary_key: Required. PrimaryConnectionString of - the endpoint. Will be obfuscated during read. + :param connection_string_primary_key: PrimaryConnectionString of the + endpoint for key-based authentication. Will be obfuscated during read. :type connection_string_primary_key: str :param connection_string_secondary_key: SecondaryConnectionString of the - endpoint. Will be obfuscated during read. + endpoint for key-based authentication. Will be obfuscated during read. :type connection_string_secondary_key: str + :param endpoint_uri: The URL of the EventHub namespace for identity-based + authentication. It must include the protocol sb:// + :type endpoint_uri: str + :param entity_path: The EventHub name in the EventHub namespace for + identity-based authentication. + :type entity_path: str """ _validation = { 'provisioning_state': {'readonly': True}, 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, - 'connection_string_primary_key': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'authentication_type': {'key': 'authenticationType', 'type': 'str'}, 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, + 'dead_letter_uri': {'key': 'deadLetterUri', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'connection_string_primary_key': {'key': 'connectionStringPrimaryKey', 'type': 'str'}, 'connection_string_secondary_key': {'key': 'connectionStringSecondaryKey', 'type': 'str'}, + 'endpoint_uri': {'key': 'endpointUri', 'type': 'str'}, + 'entity_path': {'key': 'entityPath', 'type': 'str'}, } def __init__(self, **kwargs): super(EventHub, self).__init__(**kwargs) self.connection_string_primary_key = kwargs.get('connection_string_primary_key', None) self.connection_string_secondary_key = kwargs.get('connection_string_secondary_key', None) + self.endpoint_uri = kwargs.get('endpoint_uri', None) + self.entity_path = kwargs.get('entity_path', None) self.endpoint_type = 'EventHub' diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/event_hub_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/event_hub_py3.py index d76d794ec..8c22226af 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/event_hub_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/event_hub_py3.py @@ -24,41 +24,59 @@ class EventHub(DigitalTwinsEndpointResourceProperties): 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.EndpointProvisioningState + ~controlplane.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param dead_letter_secret: Dead letter storage secret. Will be obfuscated - during read. + :param authentication_type: Specifies the authentication type being used + for connecting to the endpoint. Possible values include: 'KeyBased', + 'IdentityBased' + :type authentication_type: str or ~controlplane.models.AuthenticationType + :param dead_letter_secret: Dead letter storage secret for key-based + authentication. Will be obfuscated during read. :type dead_letter_secret: str + :param dead_letter_uri: Dead letter storage URL for identity-based + authentication. + :type dead_letter_uri: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str - :param connection_string_primary_key: Required. PrimaryConnectionString of - the endpoint. Will be obfuscated during read. + :param connection_string_primary_key: PrimaryConnectionString of the + endpoint for key-based authentication. Will be obfuscated during read. :type connection_string_primary_key: str :param connection_string_secondary_key: SecondaryConnectionString of the - endpoint. Will be obfuscated during read. + endpoint for key-based authentication. Will be obfuscated during read. :type connection_string_secondary_key: str + :param endpoint_uri: The URL of the EventHub namespace for identity-based + authentication. It must include the protocol sb:// + :type endpoint_uri: str + :param entity_path: The EventHub name in the EventHub namespace for + identity-based authentication. + :type entity_path: str """ _validation = { 'provisioning_state': {'readonly': True}, 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, - 'connection_string_primary_key': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'authentication_type': {'key': 'authenticationType', 'type': 'str'}, 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, + 'dead_letter_uri': {'key': 'deadLetterUri', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'connection_string_primary_key': {'key': 'connectionStringPrimaryKey', 'type': 'str'}, 'connection_string_secondary_key': {'key': 'connectionStringSecondaryKey', 'type': 'str'}, + 'endpoint_uri': {'key': 'endpointUri', 'type': 'str'}, + 'entity_path': {'key': 'entityPath', 'type': 'str'}, } - def __init__(self, *, connection_string_primary_key: str, dead_letter_secret: str=None, connection_string_secondary_key: str=None, **kwargs) -> None: - super(EventHub, self).__init__(dead_letter_secret=dead_letter_secret, **kwargs) + def __init__(self, *, authentication_type=None, dead_letter_secret: str=None, dead_letter_uri: str=None, connection_string_primary_key: str=None, connection_string_secondary_key: str=None, endpoint_uri: str=None, entity_path: str=None, **kwargs) -> None: + super(EventHub, self).__init__(authentication_type=authentication_type, dead_letter_secret=dead_letter_secret, dead_letter_uri=dead_letter_uri, **kwargs) self.connection_string_primary_key = connection_string_primary_key self.connection_string_secondary_key = connection_string_secondary_key + self.endpoint_uri = endpoint_uri + self.entity_path = entity_path self.endpoint_type = 'EventHub' diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information.py b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information.py new file mode 100644 index 000000000..ac990009c --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class GroupIdInformation(Model): + """The group information for creating a private endpoint on Digital Twin. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param properties: Required. + :type properties: ~controlplane.models.GroupIdInformationPropertiesModel + :param id: The resource identifier. + :type id: str + :ivar name: The resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + """ + + _validation = { + 'properties': {'required': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'properties': {'key': 'properties', 'type': 'GroupIdInformationPropertiesModel'}, + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(GroupIdInformation, self).__init__(**kwargs) + self.properties = kwargs.get('properties', None) + self.id = kwargs.get('id', None) + self.name = None + self.type = None diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties.py b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties.py new file mode 100644 index 000000000..1398514fb --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties.py @@ -0,0 +1,37 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class GroupIdInformationProperties(Model): + """The properties for a group information object. + + :param group_id: The group id + :type group_id: str + :param required_members: The required members for a specific group id. + :type required_members: list[str] + :param required_zone_names: The required DNS zones for a specific group + id. + :type required_zone_names: list[str] + """ + + _attribute_map = { + 'group_id': {'key': 'groupId', 'type': 'str'}, + 'required_members': {'key': 'requiredMembers', 'type': '[str]'}, + 'required_zone_names': {'key': 'requiredZoneNames', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(GroupIdInformationProperties, self).__init__(**kwargs) + self.group_id = kwargs.get('group_id', None) + self.required_members = kwargs.get('required_members', None) + self.required_zone_names = kwargs.get('required_zone_names', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_model.py b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_model.py new file mode 100644 index 000000000..a60bfad30 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_model.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .group_id_information_properties import GroupIdInformationProperties + + +class GroupIdInformationPropertiesModel(GroupIdInformationProperties): + """GroupIdInformationPropertiesModel. + + :param group_id: The group id + :type group_id: str + :param required_members: The required members for a specific group id. + :type required_members: list[str] + :param required_zone_names: The required DNS zones for a specific group + id. + :type required_zone_names: list[str] + """ + + _attribute_map = { + 'group_id': {'key': 'groupId', 'type': 'str'}, + 'required_members': {'key': 'requiredMembers', 'type': '[str]'}, + 'required_zone_names': {'key': 'requiredZoneNames', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(GroupIdInformationPropertiesModel, self).__init__(**kwargs) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_model_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_model_py3.py new file mode 100644 index 000000000..b472ea7a6 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_model_py3.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .group_id_information_properties_py3 import GroupIdInformationProperties + + +class GroupIdInformationPropertiesModel(GroupIdInformationProperties): + """GroupIdInformationPropertiesModel. + + :param group_id: The group id + :type group_id: str + :param required_members: The required members for a specific group id. + :type required_members: list[str] + :param required_zone_names: The required DNS zones for a specific group + id. + :type required_zone_names: list[str] + """ + + _attribute_map = { + 'group_id': {'key': 'groupId', 'type': 'str'}, + 'required_members': {'key': 'requiredMembers', 'type': '[str]'}, + 'required_zone_names': {'key': 'requiredZoneNames', 'type': '[str]'}, + } + + def __init__(self, *, group_id: str=None, required_members=None, required_zone_names=None, **kwargs) -> None: + super(GroupIdInformationPropertiesModel, self).__init__(group_id=group_id, required_members=required_members, required_zone_names=required_zone_names, **kwargs) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_py3.py new file mode 100644 index 000000000..3e44ba4a5 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_properties_py3.py @@ -0,0 +1,37 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class GroupIdInformationProperties(Model): + """The properties for a group information object. + + :param group_id: The group id + :type group_id: str + :param required_members: The required members for a specific group id. + :type required_members: list[str] + :param required_zone_names: The required DNS zones for a specific group + id. + :type required_zone_names: list[str] + """ + + _attribute_map = { + 'group_id': {'key': 'groupId', 'type': 'str'}, + 'required_members': {'key': 'requiredMembers', 'type': '[str]'}, + 'required_zone_names': {'key': 'requiredZoneNames', 'type': '[str]'}, + } + + def __init__(self, *, group_id: str=None, required_members=None, required_zone_names=None, **kwargs) -> None: + super(GroupIdInformationProperties, self).__init__(**kwargs) + self.group_id = group_id + self.required_members = required_members + self.required_zone_names = required_zone_names diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_py3.py new file mode 100644 index 000000000..4574232c2 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_py3.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class GroupIdInformation(Model): + """The group information for creating a private endpoint on Digital Twin. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param properties: Required. + :type properties: ~controlplane.models.GroupIdInformationPropertiesModel + :param id: The resource identifier. + :type id: str + :ivar name: The resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + """ + + _validation = { + 'properties': {'required': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'properties': {'key': 'properties', 'type': 'GroupIdInformationPropertiesModel'}, + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, *, properties, id: str=None, **kwargs) -> None: + super(GroupIdInformation, self).__init__(**kwargs) + self.properties = properties + self.id = id + self.name = None + self.type = None diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_response.py b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_response.py new file mode 100644 index 000000000..09c4dd92c --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_response.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class GroupIdInformationResponse(Model): + """The available private link resources for a Digital Twin. + + :param value: The list of available private link resources for a Digital + Twin. + :type value: list[~controlplane.models.GroupIdInformation] + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': '[GroupIdInformation]'}, + } + + def __init__(self, **kwargs): + super(GroupIdInformationResponse, self).__init__(**kwargs) + self.value = kwargs.get('value', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_response_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_response_py3.py new file mode 100644 index 000000000..0ae4024a9 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/group_id_information_response_py3.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class GroupIdInformationResponse(Model): + """The available private link resources for a Digital Twin. + + :param value: The list of available private link resources for a Digital + Twin. + :type value: list[~controlplane.models.GroupIdInformation] + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': '[GroupIdInformation]'}, + } + + def __init__(self, *, value=None, **kwargs) -> None: + super(GroupIdInformationResponse, self).__init__(**kwargs) + self.value = value diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/operation.py b/azext_iot/sdk/digitaltwins/controlplane/models/operation.py index 71aafcc50..234ed09af 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/operation.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/operation.py @@ -22,7 +22,7 @@ class Operation(Model): delete} :vartype name: str :param display: Operation properties display - :type display: ~azure.mgmt.digitaltwins.models.OperationDisplay + :type display: ~controlplane.models.OperationDisplay :ivar origin: The intended executor of the operation. :vartype origin: str :ivar is_data_action: If the operation is a data action (for data plane diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/operation_paged.py b/azext_iot/sdk/digitaltwins/controlplane/models/operation_paged.py index ae64e3a7d..ff83d7bef 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/operation_paged.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/operation_paged.py @@ -14,7 +14,7 @@ class OperationPaged(Paged): """ - A paging container for iterating over a list of :class:`Operation ` object + A paging container for iterating over a list of :class:`Operation ` object """ _attribute_map = { diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/operation_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/operation_py3.py index f87e6da83..c60799393 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/operation_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/operation_py3.py @@ -22,7 +22,7 @@ class Operation(Model): delete} :vartype name: str :param display: Operation properties display - :type display: ~azure.mgmt.digitaltwins.models.OperationDisplay + :type display: ~controlplane.models.OperationDisplay :ivar origin: The intended executor of the operation. :vartype origin: str :ivar is_data_action: If the operation is a data action (for data plane diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint.py b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint.py new file mode 100644 index 000000000..4cebb4acc --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PrivateEndpoint(Model): + """The private endpoint property of a private endpoint connection. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + """ + + _validation = { + 'id': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(PrivateEndpoint, self).__init__(**kwargs) + self.id = None diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection.py b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection.py new file mode 100644 index 000000000..085f56fbc --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PrivateEndpointConnection(Model): + """The private endpoint connection of a Digital Twin. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: The resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :param properties: Required. + :type properties: ~controlplane.models.PrivateEndpointConnectionProperties + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'properties': {'required': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'properties': {'key': 'properties', 'type': 'PrivateEndpointConnectionProperties'}, + } + + def __init__(self, **kwargs): + super(PrivateEndpointConnection, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None + self.properties = kwargs.get('properties', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_properties.py b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_properties.py new file mode 100644 index 000000000..9ef003a5a --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_properties.py @@ -0,0 +1,48 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .connection_properties import ConnectionProperties + + +class PrivateEndpointConnectionProperties(ConnectionProperties): + """PrivateEndpointConnectionProperties. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Pending', 'Approved', 'Rejected', 'Disconnected' + :vartype provisioning_state: str or + ~controlplane.models.ConnectionPropertiesProvisioningState + :param private_endpoint: + :type private_endpoint: + ~controlplane.models.ConnectionPropertiesPrivateEndpoint + :param group_ids: The list of group ids for the private endpoint + connection. + :type group_ids: list[str] + :param private_link_service_connection_state: + :type private_link_service_connection_state: + ~controlplane.models.ConnectionPropertiesPrivateLinkServiceConnectionState + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'private_endpoint': {'key': 'privateEndpoint', 'type': 'ConnectionPropertiesPrivateEndpoint'}, + 'group_ids': {'key': 'groupIds', 'type': '[str]'}, + 'private_link_service_connection_state': {'key': 'privateLinkServiceConnectionState', 'type': 'ConnectionPropertiesPrivateLinkServiceConnectionState'}, + } + + def __init__(self, **kwargs): + super(PrivateEndpointConnectionProperties, self).__init__(**kwargs) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_properties_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_properties_py3.py new file mode 100644 index 000000000..b1b829ca4 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_properties_py3.py @@ -0,0 +1,48 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .connection_properties_py3 import ConnectionProperties + + +class PrivateEndpointConnectionProperties(ConnectionProperties): + """PrivateEndpointConnectionProperties. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar provisioning_state: The provisioning state. Possible values include: + 'Pending', 'Approved', 'Rejected', 'Disconnected' + :vartype provisioning_state: str or + ~controlplane.models.ConnectionPropertiesProvisioningState + :param private_endpoint: + :type private_endpoint: + ~controlplane.models.ConnectionPropertiesPrivateEndpoint + :param group_ids: The list of group ids for the private endpoint + connection. + :type group_ids: list[str] + :param private_link_service_connection_state: + :type private_link_service_connection_state: + ~controlplane.models.ConnectionPropertiesPrivateLinkServiceConnectionState + """ + + _validation = { + 'provisioning_state': {'readonly': True}, + } + + _attribute_map = { + 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, + 'private_endpoint': {'key': 'privateEndpoint', 'type': 'ConnectionPropertiesPrivateEndpoint'}, + 'group_ids': {'key': 'groupIds', 'type': '[str]'}, + 'private_link_service_connection_state': {'key': 'privateLinkServiceConnectionState', 'type': 'ConnectionPropertiesPrivateLinkServiceConnectionState'}, + } + + def __init__(self, *, private_endpoint=None, group_ids=None, private_link_service_connection_state=None, **kwargs) -> None: + super(PrivateEndpointConnectionProperties, self).__init__(private_endpoint=private_endpoint, group_ids=group_ids, private_link_service_connection_state=private_link_service_connection_state, **kwargs) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_py3.py new file mode 100644 index 000000000..936c84d9d --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connection_py3.py @@ -0,0 +1,52 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PrivateEndpointConnection(Model): + """The private endpoint connection of a Digital Twin. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: The resource identifier. + :vartype id: str + :ivar name: The resource name. + :vartype name: str + :ivar type: The resource type. + :vartype type: str + :param properties: Required. + :type properties: ~controlplane.models.PrivateEndpointConnectionProperties + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True, 'pattern': r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{2,49}[a-zA-Z0-9]$'}, + 'type': {'readonly': True}, + 'properties': {'required': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'properties': {'key': 'properties', 'type': 'PrivateEndpointConnectionProperties'}, + } + + def __init__(self, *, properties, **kwargs) -> None: + super(PrivateEndpointConnection, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None + self.properties = properties diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connections_response.py b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connections_response.py new file mode 100644 index 000000000..cb53dc315 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connections_response.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PrivateEndpointConnectionsResponse(Model): + """The available private link connections for a Digital Twin. + + :param value: The list of available private link connections for a Digital + Twin. + :type value: list[~controlplane.models.PrivateEndpointConnection] + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': '[PrivateEndpointConnection]'}, + } + + def __init__(self, **kwargs): + super(PrivateEndpointConnectionsResponse, self).__init__(**kwargs) + self.value = kwargs.get('value', None) diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connections_response_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connections_response_py3.py new file mode 100644 index 000000000..51197678c --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_connections_response_py3.py @@ -0,0 +1,29 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PrivateEndpointConnectionsResponse(Model): + """The available private link connections for a Digital Twin. + + :param value: The list of available private link connections for a Digital + Twin. + :type value: list[~controlplane.models.PrivateEndpointConnection] + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': '[PrivateEndpointConnection]'}, + } + + def __init__(self, *, value=None, **kwargs) -> None: + super(PrivateEndpointConnectionsResponse, self).__init__(**kwargs) + self.value = value diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_py3.py new file mode 100644 index 000000000..4faffd817 --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/models/private_endpoint_py3.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class PrivateEndpoint(Model): + """The private endpoint property of a private endpoint connection. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: The resource identifier. + :vartype id: str + """ + + _validation = { + 'id': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + } + + def __init__(self, **kwargs) -> None: + super(PrivateEndpoint, self).__init__(**kwargs) + self.id = None diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/service_bus.py b/azext_iot/sdk/digitaltwins/controlplane/models/service_bus.py index 033ec34e8..261bb07d5 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/service_bus.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/service_bus.py @@ -24,41 +24,59 @@ class ServiceBus(DigitalTwinsEndpointResourceProperties): 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.EndpointProvisioningState + ~controlplane.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param dead_letter_secret: Dead letter storage secret. Will be obfuscated - during read. + :param authentication_type: Specifies the authentication type being used + for connecting to the endpoint. Possible values include: 'KeyBased', + 'IdentityBased' + :type authentication_type: str or ~controlplane.models.AuthenticationType + :param dead_letter_secret: Dead letter storage secret for key-based + authentication. Will be obfuscated during read. :type dead_letter_secret: str + :param dead_letter_uri: Dead letter storage URL for identity-based + authentication. + :type dead_letter_uri: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str - :param primary_connection_string: Required. PrimaryConnectionString of the - endpoint. Will be obfuscated during read. + :param primary_connection_string: PrimaryConnectionString of the endpoint + for key-based authentication. Will be obfuscated during read. :type primary_connection_string: str :param secondary_connection_string: SecondaryConnectionString of the - endpoint. Will be obfuscated during read. + endpoint for key-based authentication. Will be obfuscated during read. :type secondary_connection_string: str + :param endpoint_uri: The URL of the ServiceBus namespace for + identity-based authentication. It must include the protocol sb:// + :type endpoint_uri: str + :param entity_path: The ServiceBus Topic name for identity-based + authentication + :type entity_path: str """ _validation = { 'provisioning_state': {'readonly': True}, 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, - 'primary_connection_string': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'authentication_type': {'key': 'authenticationType', 'type': 'str'}, 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, + 'dead_letter_uri': {'key': 'deadLetterUri', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'primary_connection_string': {'key': 'primaryConnectionString', 'type': 'str'}, 'secondary_connection_string': {'key': 'secondaryConnectionString', 'type': 'str'}, + 'endpoint_uri': {'key': 'endpointUri', 'type': 'str'}, + 'entity_path': {'key': 'entityPath', 'type': 'str'}, } def __init__(self, **kwargs): super(ServiceBus, self).__init__(**kwargs) self.primary_connection_string = kwargs.get('primary_connection_string', None) self.secondary_connection_string = kwargs.get('secondary_connection_string', None) + self.endpoint_uri = kwargs.get('endpoint_uri', None) + self.entity_path = kwargs.get('entity_path', None) self.endpoint_type = 'ServiceBus' diff --git a/azext_iot/sdk/digitaltwins/controlplane/models/service_bus_py3.py b/azext_iot/sdk/digitaltwins/controlplane/models/service_bus_py3.py index 80ad6758f..30782f1c0 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/models/service_bus_py3.py +++ b/azext_iot/sdk/digitaltwins/controlplane/models/service_bus_py3.py @@ -24,41 +24,59 @@ class ServiceBus(DigitalTwinsEndpointResourceProperties): 'Provisioning', 'Deleting', 'Succeeded', 'Failed', 'Canceled', 'Deleted', 'Warning', 'Suspending', 'Restoring', 'Moving', 'Disabled' :vartype provisioning_state: str or - ~azure.mgmt.digitaltwins.models.EndpointProvisioningState + ~controlplane.models.EndpointProvisioningState :ivar created_time: Time when the Endpoint was added to DigitalTwinsInstance. :vartype created_time: datetime - :param dead_letter_secret: Dead letter storage secret. Will be obfuscated - during read. + :param authentication_type: Specifies the authentication type being used + for connecting to the endpoint. Possible values include: 'KeyBased', + 'IdentityBased' + :type authentication_type: str or ~controlplane.models.AuthenticationType + :param dead_letter_secret: Dead letter storage secret for key-based + authentication. Will be obfuscated during read. :type dead_letter_secret: str + :param dead_letter_uri: Dead letter storage URL for identity-based + authentication. + :type dead_letter_uri: str :param endpoint_type: Required. Constant filled by server. :type endpoint_type: str - :param primary_connection_string: Required. PrimaryConnectionString of the - endpoint. Will be obfuscated during read. + :param primary_connection_string: PrimaryConnectionString of the endpoint + for key-based authentication. Will be obfuscated during read. :type primary_connection_string: str :param secondary_connection_string: SecondaryConnectionString of the - endpoint. Will be obfuscated during read. + endpoint for key-based authentication. Will be obfuscated during read. :type secondary_connection_string: str + :param endpoint_uri: The URL of the ServiceBus namespace for + identity-based authentication. It must include the protocol sb:// + :type endpoint_uri: str + :param entity_path: The ServiceBus Topic name for identity-based + authentication + :type entity_path: str """ _validation = { 'provisioning_state': {'readonly': True}, 'created_time': {'readonly': True}, 'endpoint_type': {'required': True}, - 'primary_connection_string': {'required': True}, } _attribute_map = { 'provisioning_state': {'key': 'provisioningState', 'type': 'str'}, 'created_time': {'key': 'createdTime', 'type': 'iso-8601'}, + 'authentication_type': {'key': 'authenticationType', 'type': 'str'}, 'dead_letter_secret': {'key': 'deadLetterSecret', 'type': 'str'}, + 'dead_letter_uri': {'key': 'deadLetterUri', 'type': 'str'}, 'endpoint_type': {'key': 'endpointType', 'type': 'str'}, 'primary_connection_string': {'key': 'primaryConnectionString', 'type': 'str'}, 'secondary_connection_string': {'key': 'secondaryConnectionString', 'type': 'str'}, + 'endpoint_uri': {'key': 'endpointUri', 'type': 'str'}, + 'entity_path': {'key': 'entityPath', 'type': 'str'}, } - def __init__(self, *, primary_connection_string: str, dead_letter_secret: str=None, secondary_connection_string: str=None, **kwargs) -> None: - super(ServiceBus, self).__init__(dead_letter_secret=dead_letter_secret, **kwargs) + def __init__(self, *, authentication_type=None, dead_letter_secret: str=None, dead_letter_uri: str=None, primary_connection_string: str=None, secondary_connection_string: str=None, endpoint_uri: str=None, entity_path: str=None, **kwargs) -> None: + super(ServiceBus, self).__init__(authentication_type=authentication_type, dead_letter_secret=dead_letter_secret, dead_letter_uri=dead_letter_uri, **kwargs) self.primary_connection_string = primary_connection_string self.secondary_connection_string = secondary_connection_string + self.endpoint_uri = endpoint_uri + self.entity_path = entity_path self.endpoint_type = 'ServiceBus' diff --git a/azext_iot/sdk/digitaltwins/controlplane/operations/__init__.py b/azext_iot/sdk/digitaltwins/controlplane/operations/__init__.py index d55a7f121..00f62af04 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/operations/__init__.py +++ b/azext_iot/sdk/digitaltwins/controlplane/operations/__init__.py @@ -12,9 +12,13 @@ from .digital_twins_operations import DigitalTwinsOperations from .digital_twins_endpoint_operations import DigitalTwinsEndpointOperations from .operations import Operations +from .private_link_resources_operations import PrivateLinkResourcesOperations +from .private_endpoint_connections_operations import PrivateEndpointConnectionsOperations __all__ = [ 'DigitalTwinsOperations', 'DigitalTwinsEndpointOperations', 'Operations', + 'PrivateLinkResourcesOperations', + 'PrivateEndpointConnectionsOperations', ] diff --git a/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_endpoint_operations.py b/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_endpoint_operations.py index e5b62cefb..e6bbb2bc2 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_endpoint_operations.py +++ b/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_endpoint_operations.py @@ -24,7 +24,7 @@ class DigitalTwinsEndpointOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-10-31". + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-12-01". """ models = models @@ -34,7 +34,7 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-10-31" + self.api_version = "2020-12-01" self.config = config @@ -54,9 +54,9 @@ def list( overrides`. :return: An iterator like instance of DigitalTwinsEndpointResource :rtype: - ~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResourcePaged[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource] + ~controlplane.models.DigitalTwinsEndpointResourcePaged[~controlplane.models.DigitalTwinsEndpointResource] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ def internal_paging(next_link=None, raw=False): @@ -65,7 +65,7 @@ def internal_paging(next_link=None, raw=False): url = self.list.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), - 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(?`. :return: DigitalTwinsEndpointResource or ClientRawResponse if raw=true - :rtype: ~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource - or ~msrest.pipeline.ClientRawResponse + :rtype: ~controlplane.models.DigitalTwinsEndpointResource or + ~msrest.pipeline.ClientRawResponse :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ # Construct URL url = self.get.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), - 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True :rtype: - ~msrestazure.azure_operation.AzureOperationPoller[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource] + ~msrestazure.azure_operation.AzureOperationPoller[~controlplane.models.DigitalTwinsEndpointResource] or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource]] + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~controlplane.models.DigitalTwinsEndpointResource]] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ raw_result = self._create_or_update_initial( resource_group_name=resource_group_name, @@ -290,7 +290,7 @@ def _delete_initial( url = self.delete.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), - 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True :rtype: - ~msrestazure.azure_operation.AzureOperationPoller[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource] + ~msrestazure.azure_operation.AzureOperationPoller[~controlplane.models.DigitalTwinsEndpointResource] or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~azure.mgmt.digitaltwins.models.DigitalTwinsEndpointResource]] + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~controlplane.models.DigitalTwinsEndpointResource]] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ raw_result = self._delete_initial( resource_group_name=resource_group_name, diff --git a/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_operations.py b/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_operations.py index 3a9daa7d6..3dbde2af7 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_operations.py +++ b/azext_iot/sdk/digitaltwins/controlplane/operations/digital_twins_operations.py @@ -24,7 +24,7 @@ class DigitalTwinsOperations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-10-31". + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-12-01". """ models = models @@ -34,7 +34,7 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-10-31" + self.api_version = "2020-12-01" self.config = config @@ -53,16 +53,16 @@ def get( :param operation_config: :ref:`Operation configuration overrides`. :return: DigitalTwinsDescription or ClientRawResponse if raw=true - :rtype: ~azure.mgmt.digitaltwins.models.DigitalTwinsDescription or + :rtype: ~controlplane.models.DigitalTwinsDescription or ~msrest.pipeline.ClientRawResponse :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ # Construct URL url = self.get.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), - 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True :rtype: - ~msrestazure.azure_operation.AzureOperationPoller[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription] + ~msrestazure.azure_operation.AzureOperationPoller[~controlplane.models.DigitalTwinsDescription] or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription]] + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~controlplane.models.DigitalTwinsDescription]] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ raw_result = self._create_or_update_initial( resource_group_name=resource_group_name, resource_name=resource_name, - location=location, - tags=tags, + digital_twins_create=digital_twins_create, custom_headers=custom_headers, raw=True, **operation_config @@ -210,35 +207,14 @@ def get_long_running_output(response): return LROPoller(self._client, raw_result, get_long_running_output, polling_method) create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}'} - def update( - self, resource_group_name, resource_name, tags=None, custom_headers=None, raw=False, **operation_config): - """Update metadata of DigitalTwinsInstance. - - :param resource_group_name: The name of the resource group that - contains the DigitalTwinsInstance. - :type resource_group_name: str - :param resource_name: The name of the DigitalTwinsInstance. - :type resource_name: str - :param tags: Instance tags - :type tags: dict[str, str] - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: DigitalTwinsDescription or ClientRawResponse if raw=true - :rtype: ~azure.mgmt.digitaltwins.models.DigitalTwinsDescription or - ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ErrorResponseException` - """ - digital_twins_patch_description = models.DigitalTwinsPatchDescription(tags=tags) + def _update_initial( + self, resource_group_name, resource_name, digital_twins_patch_description, custom_headers=None, raw=False, **operation_config): # Construct URL url = self.update.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), - 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True + :rtype: + ~msrestazure.azure_operation.AzureOperationPoller[~controlplane.models.DigitalTwinsDescription] + or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~controlplane.models.DigitalTwinsDescription]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._update_initial( + resource_group_name=resource_group_name, + resource_name=resource_name, + digital_twins_patch_description=digital_twins_patch_description, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + def get_long_running_output(response): + deserialized = self._deserialize('DigitalTwinsDescription', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}'} @@ -287,7 +318,7 @@ def _delete_initial( url = self.delete.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), - 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True :rtype: - ~msrestazure.azure_operation.AzureOperationPoller[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription] + ~msrestazure.azure_operation.AzureOperationPoller[~controlplane.models.DigitalTwinsDescription] or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription]] + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~controlplane.models.DigitalTwinsDescription]] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ raw_result = self._delete_initial( resource_group_name=resource_group_name, @@ -386,9 +417,9 @@ def list( overrides`. :return: An iterator like instance of DigitalTwinsDescription :rtype: - ~azure.mgmt.digitaltwins.models.DigitalTwinsDescriptionPaged[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription] + ~controlplane.models.DigitalTwinsDescriptionPaged[~controlplane.models.DigitalTwinsDescription] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ def internal_paging(next_link=None, raw=False): @@ -452,9 +483,9 @@ def list_by_resource_group( overrides`. :return: An iterator like instance of DigitalTwinsDescription :rtype: - ~azure.mgmt.digitaltwins.models.DigitalTwinsDescriptionPaged[~azure.mgmt.digitaltwins.models.DigitalTwinsDescription] + ~controlplane.models.DigitalTwinsDescriptionPaged[~controlplane.models.DigitalTwinsDescription] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ def internal_paging(next_link=None, raw=False): @@ -463,7 +494,7 @@ def internal_paging(next_link=None, raw=False): url = self.list_by_resource_group.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), - 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=64, min_length=1) + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1) } url = self._client.format_url(url, **path_format_arguments) @@ -519,10 +550,10 @@ def check_name_availability( :param operation_config: :ref:`Operation configuration overrides`. :return: CheckNameResult or ClientRawResponse if raw=true - :rtype: ~azure.mgmt.digitaltwins.models.CheckNameResult or + :rtype: ~controlplane.models.CheckNameResult or ~msrest.pipeline.ClientRawResponse :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ digital_twins_instance_check_name = models.CheckNameRequest(name=name) diff --git a/azext_iot/sdk/digitaltwins/controlplane/operations/operations.py b/azext_iot/sdk/digitaltwins/controlplane/operations/operations.py index bf1060b5d..2db70e68a 100644 --- a/azext_iot/sdk/digitaltwins/controlplane/operations/operations.py +++ b/azext_iot/sdk/digitaltwins/controlplane/operations/operations.py @@ -22,7 +22,7 @@ class Operations(object): :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-10-31". + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-12-01". """ models = models @@ -32,7 +32,7 @@ def __init__(self, client, config, serializer, deserializer): self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-10-31" + self.api_version = "2020-12-01" self.config = config @@ -47,9 +47,9 @@ def list( overrides`. :return: An iterator like instance of Operation :rtype: - ~azure.mgmt.digitaltwins.models.OperationPaged[~azure.mgmt.digitaltwins.models.Operation] + ~controlplane.models.OperationPaged[~controlplane.models.Operation] :raises: - :class:`ErrorResponseException` + :class:`ErrorResponseException` """ def internal_paging(next_link=None, raw=False): diff --git a/azext_iot/sdk/digitaltwins/controlplane/operations/private_endpoint_connections_operations.py b/azext_iot/sdk/digitaltwins/controlplane/operations/private_endpoint_connections_operations.py new file mode 100644 index 000000000..5b74d372b --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/operations/private_endpoint_connections_operations.py @@ -0,0 +1,364 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrest.polling import LROPoller, NoPolling +from msrestazure.polling.arm_polling import ARMPolling + +from .. import models + + +class PrivateEndpointConnectionsOperations(object): + """PrivateEndpointConnectionsOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-12-01". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-12-01" + + self.config = config + + def list( + self, resource_group_name, resource_name, custom_headers=None, raw=False, **operation_config): + """List private endpoint connection properties. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: PrivateEndpointConnectionsResponse or ClientRawResponse if + raw=true + :rtype: ~controlplane.models.PrivateEndpointConnectionsResponse or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.list.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(?`. + :return: PrivateEndpointConnection or ClientRawResponse if raw=true + :rtype: ~controlplane.models.PrivateEndpointConnection or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True + :rtype: ~msrestazure.azure_operation.AzureOperationPoller[None] or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[None]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._delete_initial( + resource_group_name=resource_group_name, + resource_name=resource_name, + private_endpoint_connection_name=private_endpoint_connection_name, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + def get_long_running_output(response): + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}/privateEndpointConnections/{privateEndpointConnectionName}'} + + + def _create_or_update_initial( + self, resource_group_name, resource_name, private_endpoint_connection_name, properties, custom_headers=None, raw=False, **operation_config): + private_endpoint_connection = models.PrivateEndpointConnection(properties=properties) + + # Construct URL + url = self.create_or_update.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(? if raw==True + :rtype: + ~msrestazure.azure_operation.AzureOperationPoller[~controlplane.models.PrivateEndpointConnection] + or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[~controlplane.models.PrivateEndpointConnection]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._create_or_update_initial( + resource_group_name=resource_group_name, + resource_name=resource_name, + private_endpoint_connection_name=private_endpoint_connection_name, + properties=properties, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + def get_long_running_output(response): + deserialized = self._deserialize('PrivateEndpointConnection', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DigitalTwins/digitalTwinsInstances/{resourceName}/privateEndpointConnections/{privateEndpointConnectionName}'} diff --git a/azext_iot/sdk/digitaltwins/controlplane/operations/private_link_resources_operations.py b/azext_iot/sdk/digitaltwins/controlplane/operations/private_link_resources_operations.py new file mode 100644 index 000000000..44e7d4c9e --- /dev/null +++ b/azext_iot/sdk/digitaltwins/controlplane/operations/private_link_resources_operations.py @@ -0,0 +1,164 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class PrivateLinkResourcesOperations(object): + """PrivateLinkResourcesOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: Version of the DigitalTwinsInstance Management API. Constant value: "2020-12-01". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-12-01" + + self.config = config + + def list( + self, resource_group_name, resource_name, custom_headers=None, raw=False, **operation_config): + """List private link resources for given Digital Twin. + + :param resource_group_name: The name of the resource group that + contains the DigitalTwinsInstance. + :type resource_group_name: str + :param resource_name: The name of the DigitalTwinsInstance. + :type resource_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: GroupIdInformationResponse or ClientRawResponse if raw=true + :rtype: ~controlplane.models.GroupIdInformationResponse or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.list.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(?`. + :return: GroupIdInformation or ClientRawResponse if raw=true + :rtype: ~controlplane.models.GroupIdInformation or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str', max_length=90, min_length=1), + 'resourceName': self._serialize.url("resource_name", resource_name, 'str', max_length=63, min_length=3, pattern=r'^(?!-)[A-Za-z0-9-]{3,63}(?= 1: + logger.info("Waiting :{} (sec) for capacity.") + sleep(wait_in_sec) + if self.is_region_available(region=target_region, capacity=capacity): + return + interval = interval - 1 - @dt_resource_group.setter - def dt_resource_group(self, value): - self._rg = value + raise RuntimeError( + "Unavailable region DT capacity. wait(sec): {}, interval: {}, region: {}, capacity: {}".format( + wait_in_sec, interval, target_region, capacity + ) + ) + + def is_region_available(self, region, capacity: int = 1): + region_capacity = self.calculate_region_capacity + return (region_capacity.get(region, 0) + capacity) <= REGION_RESOURCE_LIMIT @property - def dt_resource_group_loc(self): - return self._rg_loc + def calculate_region_capacity(self) -> dict: + instances = self.instances = self.embedded_cli.invoke("dt list").as_json() + capacity_map = {} + for instance in instances: + cap_val = capacity_map.get(instance["location"], 0) + cap_val = cap_val + 1 + capacity_map[instance["location"]] = cap_val + + for region in REGION_LIST: + if region not in capacity_map: + capacity_map[region] = 0 + + return capacity_map + + def get_available_region(self, capacity: int = 1, skip_regions: list = None) -> str: + if not skip_regions: + skip_regions = [] + + region_capacity = self.calculate_region_capacity + + while region_capacity: + region = min(region_capacity, key=region_capacity.get) + if region not in skip_regions: + if region_capacity[region] + capacity <= REGION_RESOURCE_LIMIT: + return region + region_capacity.pop(region, None) + + raise RuntimeError( + "There are no available regions with capacity: {} for provision DT instances in subscription: {}".format( + capacity, self.current_subscription + ) + ) + + def track_instance(self, instance: dict): + self.tracked_instances.append((instance["name"], instance["resourceGroup"])) + + def tearDown(self): + for instance in self.tracked_instances: + self.embedded_cli.invoke( + "dt delete -n {} -g {} -y --no-wait".format(instance[0], instance[1]) + ) + + # 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 = 5, interval: int = 3 + ): + from time import sleep + + while interval >= 1: + logger.info( + "Waiting :{} (sec) for provisioning to complete.".format(wait_in_sec) + ) + 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"): + return refereshed_instance + + return instance diff --git a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py index a1faa580f..c4a60b93e 100644 --- a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py @@ -13,9 +13,7 @@ process_json_arg, ) from . import DTLiveScenarioTest -from . import ( - generate_resource_id -) +from . import generate_resource_id logger = get_logger(__name__) @@ -26,26 +24,30 @@ def __init__(self, test_case): super(TestDTModelLifecycle, self).__init__(test_case) def test_dt_models(self): + self.wait_for_capacity() + instance_name = generate_resource_id() models_directory = "./models" inline_model = "./models/Floor.json" component_dtmi = "dtmi:com:example:Thermostat;1" room_dtmi = "dtmi:com:example:Room;1" - self.cmd( - "dt create -n {} -g {} -l {}".format( - instance_name, self.dt_resource_group, self.dt_location - ) + self.track_instance( + self.cmd( + "dt create -n {} -g {} -l {}".format( + instance_name, self.rg, self.region + ) + ).get_output_in_json() ) self.cmd( "dt role-assignment create -n {} -g {} --assignee {} --role '{}'".format( - instance_name, self.dt_resource_group, self.current_user, self.role_map["owner"] + instance_name, self.rg, self.current_user, self.role_map["owner"] ) ) # Wait for RBAC to catch-up - sleep(20) + sleep(60) create_models_output = self.cmd( "dt model create -n {} --from-directory '{}'".format( @@ -65,9 +67,7 @@ def test_dt_models(self): assert model["id"] list_models_output = self.cmd( - "dt model list -n {} -g {} --definition".format( - instance_name, self.dt_resource_group - ) + "dt model list -n {} -g {} --definition".format(instance_name, self.rg) ).get_output_in_json() assert len(list_models_output) == len(create_models_output) for model in list_models_output: @@ -76,7 +76,9 @@ def test_dt_models(self): model_dependencies_output = self.cmd( "dt model list -n {} -g {} --dependencies-for '{}'".format( - instance_name, self.dt_resource_group, room_dtmi, + instance_name, + self.rg, + room_dtmi, ) ).get_output_in_json() assert len(model_dependencies_output) == 2 @@ -89,7 +91,7 @@ def test_dt_models(self): model_show_def_output = self.cmd( "dt model show -n {} -g {} --dtmi '{}' --definition".format( - instance_name, self.dt_resource_group, model["id"] + instance_name, self.rg, model["id"] ) ).get_output_in_json() @@ -140,17 +142,12 @@ def test_dt_models(self): == 0 ) - self.cmd( - "dt delete -n {} -g {}".format(instance_name, self.dt_resource_group) - ) - -def assert_create_models_attributes( - result, directory_path=None, models=None, return_metadata=True -): +def assert_create_models_attributes(result, directory_path=None, models=None): if not any([directory_path, models]): raise ValueError("Provide directory_path or models.") + local_test_models = [] if directory_path: local_test_models = _get_models_from_directory(directory_path) diff --git a/azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py new file mode 100644 index 000000000..714adfc40 --- /dev/null +++ b/azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py @@ -0,0 +1,176 @@ +# 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 . import DTLiveScenarioTest +from . import generate_resource_id, generate_generic_id + +logger = get_logger(__name__) + + +class TestDTPrivateLinksLifecycle(DTLiveScenarioTest): + def __init__(self, test_case): + super(TestDTPrivateLinksLifecycle, self).__init__(test_case) + + def test_dt_privatelinks(self): + self.wait_for_capacity() + + instance_name = generate_resource_id() + group_id = "API" + create_output = self.cmd( + "dt create -n {} -g {} -l {}".format( + instance_name, + self.rg, + self.region, + ) + ).get_output_in_json() + self.track_instance(create_output) + + # Fail test if hostName missing + assert create_output.get( + "hostName" + ), "Service failed to provision DT instance: {}.".format(instance_name) + + list_priv_links = self.cmd( + "dt network private-link list -n {} -g {}".format( + instance_name, + self.rg, + ) + ).get_output_in_json() + assert len(list_priv_links) > 0 + + show_api_priv_link = self.cmd( + "dt network private-link show -n {} -g {} --ln {}".format( + instance_name, self.rg, group_id + ) + ).get_output_in_json() + assert show_api_priv_link["name"] == group_id + assert ( + show_api_priv_link["type"] + == "Microsoft.DigitalTwins/digitalTwinsInstances/privateLinkResources" + ) + + connection_name = generate_generic_id() + endpoint_name = generate_generic_id() + dt_instance_id = create_output["id"] + vnet_name = generate_generic_id() + subnet_name = generate_generic_id() + + # Create VNET + self.cmd( + "network vnet create -n {} -g {} --subnet-name {}".format( + vnet_name, self.rg, subnet_name + ), + checks=self.check("length(newVNet.subnets)", 1), + ) + self.cmd( + "network vnet subnet update -n {} --vnet-name {} -g {} " + "--disable-private-endpoint-network-policies true".format( + subnet_name, vnet_name, self.rg + ), + checks=self.check("privateEndpointNetworkPolicies", "Disabled"), + ) + + create_priv_endpoint_result = self.embedded_cli.invoke( + "network private-endpoint create --connection-name {} -n {} --private-connection-resource-id '{}'" + " --group-id {} -g {} --vnet-name {} --subnet {} --manual-request".format( + connection_name, + endpoint_name, + dt_instance_id, + group_id, + self.rg, + vnet_name, + subnet_name, + ) + ) + + if not create_priv_endpoint_result.success(): + raise RuntimeError( + "Failed to configure private-endpoint for DT instance: {}".format( + instance_name + ) + ) + + list_priv_endpoints = self.cmd( + "dt network private-endpoint connection list -n {} -g {}".format( + instance_name, + self.rg, + ) + ).get_output_in_json() + assert len(list_priv_endpoints) > 0 + + instance_connection_id = list_priv_endpoints[-1]["name"] + + show_priv_endpoint = self.cmd( + "dt network private-endpoint connection show -n {} -g {} --cn {}".format( + instance_name, self.rg, instance_connection_id + ) + ).get_output_in_json() + assert show_priv_endpoint["name"] == instance_connection_id + assert ( + show_priv_endpoint["type"] + == "Microsoft.DigitalTwins/digitalTwinsInstances/privateEndpointConnections" + ) + assert show_priv_endpoint["properties"]["provisioningState"] == "Succeeded" + + # Force manual approval + assert ( + show_priv_endpoint["properties"]["privateLinkServiceConnectionState"]["status"] + == "Pending" + ) + + random_desc_approval = "{} {}".format( + generate_generic_id(), generate_generic_id() + ) + set_connection_output = self.cmd( + "dt network private-endpoint connection set -n {} -g {} --cn {} --status Approved --desc '{}'".format( + instance_name, self.rg, instance_connection_id, random_desc_approval + ) + ).get_output_in_json() + assert ( + set_connection_output["properties"]["privateLinkServiceConnectionState"]["status"] + == "Approved" + ) + assert ( + set_connection_output["properties"]["privateLinkServiceConnectionState"]["description"] + == random_desc_approval + ) + + random_desc_rejected = "{} {}".format( + generate_generic_id(), generate_generic_id() + ) + set_connection_output = self.cmd( + "dt network private-endpoint connection set -n {} -g {} --cn {} --status Rejected --desc '{}'".format( + 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 delete -n {} -g {} --cn {} -y".format( + instance_name, self.rg, instance_connection_id + ) + ) + + list_priv_endpoints = self.cmd( + "dt network private-endpoint connection list -n {} -g {}".format( + instance_name, + self.rg, + ) + ).get_output_in_json() + assert len(list_priv_endpoints) == 0 + + # TODO clean-up optimization + + self.cmd("network private-endpoint delete -n {} -g {} ".format(endpoint_name, self.rg)) + self.cmd("network vnet delete -n {} -g {} ".format(vnet_name, self.rg)) 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 b2f682896..0734fce04 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -5,15 +5,18 @@ # -------------------------------------------------------------------------------------------- import pytest +from typing import List +from collections import namedtuple from time import sleep from knack.log import get_logger -from azext_iot.digitaltwins.common import ADTEndpointType +from azext_iot.digitaltwins.common import ADTEndpointAuthType, ADTEndpointType from azext_iot.tests.settings import DynamoSettings from . import DTLiveScenarioTest from . import ( MOCK_RESOURCE_TAGS, MOCK_RESOURCE_TAGS_DICT, MOCK_DEAD_LETTER_SECRET, + MOCK_DEAD_LETTER_ENDPOINT, generate_resource_id, ) @@ -55,23 +58,24 @@ def __init__(self, test_case): super(TestDTResourceLifecycle, self).__init__(test_case) def test_dt_resource(self): - instance_names = [generate_resource_id(), generate_resource_id()] - dt_location_custom = "eastus2euap" + self.wait_for_capacity(capacity=3) + instance_names = [generate_resource_id(), generate_resource_id()] create_output = self.cmd( "dt create -n {} -g {} -l {} --tags {}".format( instance_names[0], - self.dt_resource_group, - self.dt_location, + self.rg, + self.region, MOCK_RESOURCE_TAGS, ) ).get_output_in_json() + self.track_instance(create_output) assert_common_resource_attributes( - create_output, + self.wait_for_hostname(create_output), instance_names[0], - self.dt_resource_group, - self.dt_location, + self.rg, + self.region, MOCK_RESOURCE_TAGS_DICT, ) @@ -79,26 +83,26 @@ def test_dt_resource(self): self.cmd( "dt create -n {} -g {} -l {} --tags {}".format( instance_names[0], - self.dt_resource_group, - dt_location_custom, + self.rg, + self.get_available_region(1, skip_regions=[self.region]), MOCK_RESOURCE_TAGS, ), expect_failure=True, ) # No location specified. Use the resource group location. - create_output = self.cmd( - "dt create -n {} -g {}".format( - instance_names[1], self.dt_resource_group - ) + create_msi_output = self.cmd( + "dt create -n {} -g {} --assign-identity".format(instance_names[1], self.rg) ).get_output_in_json() + self.track_instance(create_msi_output) assert_common_resource_attributes( - create_output, + self.wait_for_hostname(create_msi_output), instance_names[1], - self.dt_resource_group, - self.dt_resource_group_loc, - None, + self.rg, + self.rg_region, + tags=None, + assign_identity=True, ) show_output = self.cmd( @@ -108,21 +112,22 @@ def test_dt_resource(self): assert_common_resource_attributes( show_output, instance_names[0], - self.dt_resource_group, - self.dt_location, + self.rg, + self.region, MOCK_RESOURCE_TAGS_DICT, ) - show_output = self.cmd( - "dt show -n {} -g {}".format(instance_names[1], self.dt_resource_group) + show_msi_output = self.cmd( + "dt show -n {} -g {}".format(instance_names[1], self.rg) ).get_output_in_json() assert_common_resource_attributes( - show_output, + show_msi_output, instance_names[1], - self.dt_resource_group, - self.dt_location, - None, + self.rg, + self.rg_region, + tags=None, + assign_identity=True, ) list_output = self.cmd("dt list").get_output_in_json() @@ -130,27 +135,35 @@ def test_dt_resource(self): assert len(filtered_list) == len(instance_names) list_group_output = self.cmd( - "dt list -g {}".format(self.dt_resource_group) + "dt list -g {}".format(self.rg) ).get_output_in_json() filtered_group_list = filter_dt_list(list_group_output, instance_names) assert len(filtered_group_list) == len(instance_names) # Delete does not currently return output - self.cmd("dt delete -n {}".format(instance_names[0])) + # Delete no blocking self.cmd( - "dt delete -n {} -g {}".format(instance_names[1], self.dt_resource_group) + "dt delete -n {} -g {} -y --no-wait".format(instance_names[1], self.rg) ) + # Delete while blocking + self.cmd("dt delete -n {} -y".format(instance_names[0])) + def test_dt_rbac(self): + self.wait_for_capacity() + rbac_assignee_owner = self.current_user rbac_assignee_reader = self.current_user rbac_instance_name = generate_resource_id() - self.cmd( + rbac_instance = self.cmd( "dt create -n {} -g {} -l {}".format( - rbac_instance_name, self.dt_resource_group, self.dt_location, + rbac_instance_name, + self.rg, + self.region, ) - ) + ).get_output_in_json() + self.track_instance(rbac_instance) assert ( len( @@ -168,7 +181,10 @@ def test_dt_rbac(self): ).get_output_in_json() assert_common_rbac_attributes( - assign_output, rbac_instance_name, "owner", rbac_assignee_owner, + assign_output, + rbac_instance_name, + "owner", + rbac_assignee_owner, ) assign_output = self.cmd( @@ -176,12 +192,15 @@ def test_dt_rbac(self): rbac_instance_name, rbac_assignee_reader, self.role_map["reader"], - self.dt_resource_group, + self.rg, ) ).get_output_in_json() assert_common_rbac_attributes( - assign_output, rbac_instance_name, "reader", rbac_assignee_reader, + assign_output, + rbac_instance_name, + "reader", + rbac_assignee_reader, ) list_assigned_output = self.cmd( @@ -190,19 +209,18 @@ def test_dt_rbac(self): assert len(list_assigned_output) == 2 - # role-assignment delete does not currently return output - # Remove specific role assignment (reader) for assignee + # Role-assignment delete does not currently return output self.cmd( "dt role-assignment delete -n {} --assignee {} --role '{}'".format( - rbac_instance_name, rbac_assignee_owner, self.role_map["reader"], + rbac_instance_name, + rbac_assignee_owner, + self.role_map["reader"], ) ) list_assigned_output = self.cmd( - "dt role-assignment list -n {} -g {}".format( - rbac_instance_name, self.dt_resource_group - ) + "dt role-assignment list -n {} -g {}".format(rbac_instance_name, self.rg) ).get_output_in_json() assert len(list_assigned_output) == 1 @@ -215,26 +233,47 @@ def test_dt_rbac(self): ) list_assigned_output = self.cmd( - "dt role-assignment list -n {} -g {}".format( - rbac_instance_name, self.dt_resource_group - ) + "dt role-assignment list -n {} -g {}".format(rbac_instance_name, self.rg) ).get_output_in_json() assert len(list_assigned_output) == 0 - self.cmd("dt delete -n {}".format(rbac_instance_name)) - @pytest.mark.skipif( not run_endpoint_route_tests, reason="All azext_dt_ep_* env vars are required for endpoint and route tests.", ) def test_dt_endpoints_routes(self): + self.wait_for_capacity() endpoints_instance_name = generate_resource_id() - self.cmd( - "dt create -n {} -g {} -l {}".format( - endpoints_instance_name, self.dt_resource_group, self.dt_location, + target_scope_role = "Contributor" + + sb_topic_resource_id = self.embedded_cli.invoke( + "servicebus topic show --namespace-name {} -n {} -g {}".format( + settings.env.azext_dt_ep_servicebus_namespace, + settings.env.azext_dt_ep_servicebus_topic, + settings.env.azext_dt_ep_rg, ) - ) + ).as_json()["id"] + + eh_resource_id = self.embedded_cli.invoke( + "eventhubs eventhub show --namespace-name {} -n {} -g {}".format( + settings.env.azext_dt_ep_eventhub_namespace, + settings.env.azext_dt_ep_eventhub_topic, + settings.env.azext_dt_ep_rg, + ) + ).as_json()["id"] + + endpoint_instance = self.cmd( + "dt create -n {} -g {} -l {} --assign-identity --scopes {} {} --role {}".format( + endpoints_instance_name, + self.rg, + self.region, + sb_topic_resource_id, + eh_resource_id, + target_scope_role, + ) + ).get_output_in_json() + self.track_instance(endpoint_instance) # Setup RBAC so we can interact with routes self.cmd( @@ -242,12 +281,16 @@ def test_dt_endpoints_routes(self): endpoints_instance_name, self.current_user, self.role_map["owner"], - self.dt_resource_group, + self.rg, ) ) - sleep(60) # Wait for service to catch-up + sleep(30) # Wait for service to catch-up + EndpointTuple = namedtuple( + "endpoint_tuple", ["endpoint_name", "endpoint_type", "auth_type"] + ) + endpoint_tuple_collection: List[EndpointTuple] = [] list_ep_output = self.cmd( "dt endpoint list -n {}".format(endpoints_instance_name) ).get_output_in_json() @@ -257,21 +300,30 @@ def test_dt_endpoints_routes(self): eventgrid_topic = settings.env.azext_dt_ep_eventgrid_topic eventgrid_endpoint = "myeventgridendpoint" - logger.debug("Adding eventgrid endpoint...") + logger.debug("Adding key based eventgrid endpoint...") add_ep_output = self.cmd( "dt endpoint create eventgrid -n {} -g {} --egg {} --egt {} --en {} --dsu {}".format( endpoints_instance_name, - self.dt_resource_group, + self.rg, eventgrid_rg, eventgrid_topic, eventgrid_endpoint, - MOCK_DEAD_LETTER_SECRET + MOCK_DEAD_LETTER_SECRET, ) ).get_output_in_json() assert_common_endpoint_attributes( add_ep_output, eventgrid_endpoint, ADTEndpointType.eventgridtopic, + dead_letter_secret=MOCK_DEAD_LETTER_SECRET, + ) + + endpoint_tuple_collection.append( + EndpointTuple( + eventgrid_endpoint, + ADTEndpointType.eventgridtopic, + ADTEndpointAuthType.keybased, + ) ) servicebus_rg = settings.env.azext_dt_ep_rg @@ -279,9 +331,10 @@ def test_dt_endpoints_routes(self): servicebus_policy = settings.env.azext_dt_ep_servicebus_policy servicebus_topic = settings.env.azext_dt_ep_servicebus_topic servicebus_endpoint = "myservicebusendpoint" + servicebus_endpoint_msi = "{}identity".format(servicebus_endpoint) - logger.debug("Adding servicebus topic endpoint...") - add_ep_output = self.cmd( + logger.debug("Adding key based servicebus topic endpoint...") + add_ep_sb_key_output = self.cmd( "dt endpoint create servicebus -n {} --sbg {} --sbn {} --sbp {} --sbt {} --en {} --dsu {}".format( endpoints_instance_name, servicebus_rg, @@ -289,14 +342,50 @@ def test_dt_endpoints_routes(self): servicebus_policy, servicebus_topic, servicebus_endpoint, - MOCK_DEAD_LETTER_SECRET + MOCK_DEAD_LETTER_SECRET, ) ).get_output_in_json() assert_common_endpoint_attributes( - add_ep_output, + add_ep_sb_key_output, servicebus_endpoint, - ADTEndpointType.servicebus, + endpoint_type=ADTEndpointType.servicebus, + auth_type=ADTEndpointAuthType.keybased, + dead_letter_secret=MOCK_DEAD_LETTER_SECRET, + ) + endpoint_tuple_collection.append( + EndpointTuple( + servicebus_endpoint, + ADTEndpointType.servicebus, + ADTEndpointAuthType.keybased, + ) + ) + + logger.debug("Adding identity based servicebus topic endpoint...") + add_ep_sb_identity_output = self.cmd( + "dt endpoint create servicebus -n {} --sbg {} --sbn {} --sbt {} --en {} --du {} --auth-type IdentityBased".format( + endpoints_instance_name, + servicebus_rg, + servicebus_namespace, + servicebus_topic, + servicebus_endpoint_msi, + MOCK_DEAD_LETTER_ENDPOINT, + ) + ).get_output_in_json() + + assert_common_endpoint_attributes( + add_ep_sb_identity_output, + servicebus_endpoint_msi, + endpoint_type=ADTEndpointType.servicebus, + auth_type=ADTEndpointAuthType.identitybased, + dead_letter_endpoint=MOCK_DEAD_LETTER_ENDPOINT, + ) + endpoint_tuple_collection.append( + EndpointTuple( + servicebus_endpoint_msi, + ADTEndpointType.servicebus, + ADTEndpointAuthType.identitybased, + ) ) eventhub_rg = settings.env.azext_dt_ep_rg @@ -304,8 +393,9 @@ def test_dt_endpoints_routes(self): eventhub_policy = settings.env.azext_dt_ep_eventhub_policy eventhub_topic = settings.env.azext_dt_ep_eventhub_topic eventhub_endpoint = "myeventhubendpoint" + eventhub_endpoint_msi = "{}identity".format(eventhub_endpoint) - logger.debug("Adding eventhub endpoint...") + logger.debug("Adding key based eventhub endpoint...") add_ep_output = self.cmd( "dt endpoint create eventhub -n {} --ehg {} --ehn {} --ehp {} --eh {} --ehs {} --en {} --dsu {}".format( endpoints_instance_name, @@ -315,42 +405,75 @@ def test_dt_endpoints_routes(self): eventhub_topic, self.current_subscription, eventhub_endpoint, - MOCK_DEAD_LETTER_SECRET + MOCK_DEAD_LETTER_SECRET, ) ).get_output_in_json() assert_common_endpoint_attributes( - add_ep_output, eventhub_endpoint, ADTEndpointType.eventhub + add_ep_output, + eventhub_endpoint, + ADTEndpointType.eventhub, + auth_type=ADTEndpointAuthType.keybased, + dead_letter_secret=MOCK_DEAD_LETTER_SECRET, ) - - show_ep_output = self.cmd( - "dt endpoint show -n {} --en {}".format( - endpoints_instance_name, eventhub_endpoint, + endpoint_tuple_collection.append( + EndpointTuple( + eventhub_endpoint, + ADTEndpointType.eventhub, + ADTEndpointAuthType.keybased, ) - ).get_output_in_json() - - assert_common_endpoint_attributes( - show_ep_output, eventhub_endpoint, ADTEndpointType.eventhub ) - show_ep_output = self.cmd( - "dt endpoint show -n {} -g {} --en {}".format( - endpoints_instance_name, self.dt_resource_group, servicebus_endpoint, + logger.debug("Adding identity based eventhub endpoint...") + add_ep_output = self.cmd( + "dt endpoint create eventhub -n {} --ehg {} --ehn {} --eh {} --ehs {} --en {} --du {} " + "--auth-type IdentityBased".format( + endpoints_instance_name, + eventhub_rg, + eventhub_namespace, + eventhub_topic, + self.current_subscription, + eventhub_endpoint_msi, + MOCK_DEAD_LETTER_ENDPOINT, ) ).get_output_in_json() assert_common_endpoint_attributes( - show_ep_output, - servicebus_endpoint, - ADTEndpointType.servicebus, + add_ep_output, + eventhub_endpoint_msi, + endpoint_type=ADTEndpointType.eventhub, + auth_type=ADTEndpointAuthType.identitybased, + dead_letter_endpoint=MOCK_DEAD_LETTER_ENDPOINT, + ) + endpoint_tuple_collection.append( + EndpointTuple( + eventhub_endpoint_msi, + ADTEndpointType.eventhub, + ADTEndpointAuthType.identitybased, + ) ) - list_ep_output = self.cmd( - "dt endpoint list -n {} -g {}".format( - endpoints_instance_name, self.dt_resource_group + for ep in endpoint_tuple_collection: + is_last = ep.endpoint_name == endpoint_tuple_collection[-1].endpoint_name + show_ep_output = self.cmd( + "dt endpoint show -n {} --en {} {}".format( + endpoints_instance_name, + ep.endpoint_name, + "-g {}".format(self.rg) if is_last else "", + ) + ).get_output_in_json() + + assert_common_endpoint_attributes( + show_ep_output, + ep.endpoint_name, + endpoint_type=ep.endpoint_type, + auth_type=ep.auth_type, ) + + list_ep_output = self.cmd( + "dt endpoint list -n {} -g {}".format(endpoints_instance_name, self.rg) ).get_output_in_json() - assert len(list_ep_output) == 3 + assert len(list_ep_output) == 5 endpoint_names = [eventgrid_endpoint, servicebus_endpoint, eventhub_endpoint] filter_values = ["", "false", "type = Microsoft.DigitalTwins.Twin.Create"] @@ -371,7 +494,7 @@ def test_dt_endpoints_routes(self): route_name, endpoint_name, filter_value, - "-g {}".format(self.dt_resource_group) if is_last else "", + "-g {}".format(self.rg) if is_last else "", ) ).get_output_in_json() @@ -383,7 +506,7 @@ def test_dt_endpoints_routes(self): "dt route show -n {} --rn {} {}".format( endpoints_instance_name, route_name, - "-g {}".format(self.dt_resource_group) if is_last else "", + "-g {}".format(self.rg) if is_last else "", ) ).get_output_in_json() @@ -392,9 +515,7 @@ def test_dt_endpoints_routes(self): ) list_routes_output = self.cmd( - "dt route list -n {} -g {}".format( - endpoints_instance_name, self.dt_resource_group - ) + "dt route list -n {} -g {}".format(endpoints_instance_name, self.rg) ).get_output_in_json() assert len(list_routes_output) == 3 @@ -405,49 +526,50 @@ def test_dt_endpoints_routes(self): "dt route delete -n {} --rn {} {}".format( endpoints_instance_name, route_name, - "-g {}".format(self.dt_resource_group) if is_last else "", + "-g {}".format(self.rg) if is_last else "", ) ) list_routes_output = self.cmd( - "dt route list -n {} -g {}".format( - endpoints_instance_name, self.dt_resource_group - ) + "dt route list -n {} -g {}".format(endpoints_instance_name, self.rg) ).get_output_in_json() assert len(list_routes_output) == 0 - # Unfortuntely the service does not yet know how to delete child resouces - # of a dt parent automatically. So we have to explictly delete every endpoint first. - - for endpoint_name in endpoint_names: - logger.debug("Cleaning up {} endpoint...".format(endpoint_name)) - is_last = endpoint_name == endpoint_names[-1] + for ep in endpoint_tuple_collection: + logger.debug("Deleting endpoint {}...".format(ep.endpoint_name)) + is_last = ep.endpoint_name == endpoint_tuple_collection[-1].endpoint_name self.cmd( - "dt endpoint delete -n {} --en {} {}".format( + "dt endpoint delete -y -n {} --en {} {}".format( endpoints_instance_name, - endpoint_name, - "-g {}".format(self.dt_resource_group) if is_last else "", + ep.endpoint_name, + "-g {} --no-wait".format(self.rg) if is_last else "", ) ) list_endpoint_output = self.cmd( - "dt endpoint list -n {} -g {}".format( - endpoints_instance_name, self.dt_resource_group - ) + "dt endpoint list -n {} -g {}".format(endpoints_instance_name, self.rg) ).get_output_in_json() - assert len(list_endpoint_output) == 0 - self.cmd( - "dt delete -n {} -g {}".format( - endpoints_instance_name, self.dt_resource_group + assert ( + len(list_endpoint_output) == 0 + or len( + [ + ep + for ep in list_endpoint_output + if ep["properties"]["provisioningState"].lower() != "deleting" + ] ) + == 0 ) def assert_common_resource_attributes( - instance_output, resource_id, group_id, location, tags + instance_output, resource_id, group_id, location, tags=None, assign_identity=False ): assert instance_output["createdTime"] - assert instance_output["hostName"].startswith(resource_id) + hostname = instance_output.get("hostName") + + assert hostname, "Provisioned instance is missing hostName." + assert hostname.startswith(resource_id) assert instance_output["location"] == location assert instance_output["id"].endswith(resource_id) assert instance_output["lastUpdatedTime"] @@ -455,7 +577,18 @@ def assert_common_resource_attributes( assert instance_output["provisioningState"] == "Succeeded" assert instance_output["resourceGroup"] == group_id assert instance_output["type"] == "Microsoft.DigitalTwins/digitalTwinsInstances" - assert instance_output["tags"] == tags + + if tags: + assert instance_output["tags"] == tags + + if not assign_identity: + assert instance_output["identity"] is None + return + + assert instance_output["identity"]["principalId"] + assert instance_output["identity"]["tenantId"] + # Currently only SystemAssigned identity is supported. + assert instance_output["identity"]["type"] == "SystemAssigned" def assert_common_route_attributes( @@ -467,7 +600,12 @@ def assert_common_route_attributes( def assert_common_endpoint_attributes( - endpoint_output, endpoint_name, endpoint_type, dead_letter_secret=None + endpoint_output, + endpoint_name, + endpoint_type, + dead_letter_secret=None, + dead_letter_endpoint=None, + auth_type=ADTEndpointAuthType.keybased, ): assert endpoint_output["id"].endswith("/{}".format(endpoint_name)) assert ( @@ -475,12 +613,16 @@ def assert_common_endpoint_attributes( == "Microsoft.DigitalTwins/digitalTwinsInstances/endpoints" ) assert endpoint_output["resourceGroup"] - assert endpoint_output["properties"]["provisioningState"] assert endpoint_output["properties"]["createdTime"] + if dead_letter_secret: assert endpoint_output["properties"]["deadLetterSecret"] + if dead_letter_endpoint: + assert endpoint_output["properties"]["deadLetterUri"] + + # Currently DT -> EventGrid is only key based. if endpoint_type == ADTEndpointType.eventgridtopic: assert endpoint_output["properties"]["topicEndpoint"] assert endpoint_output["properties"]["accessKey1"] @@ -488,13 +630,21 @@ def assert_common_endpoint_attributes( assert endpoint_output["properties"]["endpointType"] == "EventGrid" return if endpoint_type == ADTEndpointType.servicebus: - assert endpoint_output["properties"]["primaryConnectionString"] - assert endpoint_output["properties"]["secondaryConnectionString"] + if auth_type == ADTEndpointAuthType.keybased: + assert endpoint_output["properties"]["primaryConnectionString"] + assert endpoint_output["properties"]["secondaryConnectionString"] + if auth_type == ADTEndpointAuthType.identitybased: + assert endpoint_output["properties"]["endpointUri"] + assert endpoint_output["properties"]["entityPath"] assert endpoint_output["properties"]["endpointType"] == "ServiceBus" return if endpoint_type == ADTEndpointType.eventhub: - assert endpoint_output["properties"]["connectionStringPrimaryKey"] - assert endpoint_output["properties"]["connectionStringSecondaryKey"] + if auth_type == ADTEndpointAuthType.keybased: + assert endpoint_output["properties"]["connectionStringPrimaryKey"] + assert endpoint_output["properties"]["connectionStringSecondaryKey"] + if auth_type == ADTEndpointAuthType.identitybased: + assert endpoint_output["properties"]["endpointUri"] + assert endpoint_output["properties"]["entityPath"] assert endpoint_output["properties"]["endpointType"] == "EventHub" return 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 33b7ab2ea..33280ef19 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -22,6 +22,7 @@ def __init__(self, test_case): super(TestDTTwinLifecycle, self).__init__(test_case) def test_dt_twin(self): + self.wait_for_capacity() instance_name = generate_resource_id() models_directory = "./models" floor_dtmi = "dtmi:com:example:Floor;1" @@ -30,15 +31,17 @@ def test_dt_twin(self): room_twin_id = "myroom" thermostat_component_id = "Thermostat" - self.cmd( - "dt create -n {} -g {} -l {}".format( - instance_name, self.dt_resource_group, self.dt_location - ) + self.track_instance( + self.cmd( + "dt create -n {} -g {} -l {}".format( + instance_name, self.rg, self.region + ) + ).get_output_in_json() ) self.cmd( "dt role-assignment create -n {} -g {} --assignee {} --role '{}'".format( - instance_name, self.dt_resource_group, self.current_user, self.role_map["owner"] + instance_name, self.rg, self.current_user, self.role_map["owner"] ) ) # Wait for RBAC to catch-up @@ -93,7 +96,7 @@ def test_dt_twin(self): self.cmd( "dt twin create -n {} -g {} --dtmi {} --twin-id {}".format( instance_name, - self.dt_resource_group, + self.rg, room_dtmi, room_twin_id ), @@ -104,7 +107,7 @@ def test_dt_twin(self): min_room_twin = self.cmd( "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( instance_name, - self.dt_resource_group, + self.rg, room_dtmi, room_twin_id, "{emptyThermostatComponentJson}", @@ -122,7 +125,7 @@ def test_dt_twin(self): room_twin = self.cmd( "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( instance_name, - self.dt_resource_group, + self.rg, room_dtmi, room_twin_id, "{tempAndThermostatComponentJson}", @@ -142,7 +145,7 @@ def test_dt_twin(self): thermostat_component = self.cmd( "dt twin component show -n {} -g {} --twin-id {} --component {}".format( instance_name, - self.dt_resource_group, + self.rg, room_twin_id, thermostat_component_id, ) @@ -156,7 +159,7 @@ def test_dt_twin(self): self.cmd( "dt twin component update -n {} -g {} --twin-id {} --component {} --json-patch '{}'".format( instance_name, - self.dt_resource_group, + self.rg, room_twin_id, thermostat_component_id, "{thermostatJsonPatch}", @@ -166,7 +169,7 @@ def test_dt_twin(self): thermostat_component = self.cmd( "dt twin component show -n {} -g {} --twin-id {} --component {}".format( instance_name, - self.dt_resource_group, + self.rg, room_twin_id, thermostat_component_id, ) @@ -187,7 +190,7 @@ def test_dt_twin(self): "dt twin show -n {} --twin-id {} {}".format( instance_name, twin_tuple[0], - "-g {}".format(self.dt_resource_group) + "-g {}".format(self.rg) if twins_id_list[-1] == twin_tuple else "", ) @@ -213,7 +216,7 @@ def test_dt_twin(self): twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins'".format( - instance_name, self.dt_resource_group + instance_name, self.rg ) ).get_output_in_json() assert len(twin_query_result["result"]) == 2 @@ -231,7 +234,7 @@ def test_dt_twin(self): "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --twin-id {} " "--target-twin-id {} --properties '{}'".format( instance_name, - self.dt_resource_group, + self.rg, relationship_id, relationship, floor_twin_id, @@ -252,7 +255,7 @@ def test_dt_twin(self): twin_relationship_show_result = self.cmd( "dt twin relationship show -n {} -g {} --twin-id {} --relationship-id {}".format( instance_name, - self.dt_resource_group, + self.rg, floor_twin_id, relationship_id, ) @@ -271,7 +274,7 @@ def test_dt_twin(self): "dt twin relationship update -n {} -g {} --relationship-id {} --twin-id {} " "--json-patch '{}'".format( instance_name, - self.dt_resource_group, + self.rg, relationship_id, floor_twin_id, "{relationshipJsonPatch}", @@ -293,7 +296,7 @@ def test_dt_twin(self): twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} -g {} --twin-id {} --relationship {}".format( instance_name, - self.dt_resource_group, + self.rg, floor_twin_id, relationship, ) @@ -331,7 +334,7 @@ def test_dt_twin(self): twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} -g {} --twin-id {} --kind {}".format( instance_name, - self.dt_resource_group, + self.rg, floor_twin_id, relationship, ) @@ -345,7 +348,7 @@ def test_dt_twin(self): self.cmd( "dt twin telemetry send -n {} -g {} --twin-id {} --telemetry '{}'".format( instance_name, - self.dt_resource_group, + self.rg, room_twin_id, "{telemetryJson}", ) @@ -354,7 +357,7 @@ def test_dt_twin(self): self.cmd( "dt twin telemetry send -n {} -g {} --twin-id {} --component {} --telemetry '{}'".format( instance_name, - self.dt_resource_group, + self.rg, room_twin_id, thermostat_component_id, "{telemetryJson}", @@ -367,7 +370,7 @@ def test_dt_twin(self): "dt twin delete -n {} --twin-id {} {}".format( instance_name, twin_tuple[0], - "-g {}".format(self.dt_resource_group) + "-g {}".format(self.rg) if twins_id_list[-1] == twin_tuple else "", ) @@ -375,16 +378,12 @@ def test_dt_twin(self): sleep(10) # Wait for API to catch up twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins' --cost".format( - instance_name, self.dt_resource_group + instance_name, self.rg ) ).get_output_in_json() assert len(twin_query_result["result"]) == 0 assert twin_query_result["cost"] - self.cmd( - "dt delete -n {} -g {}".format(instance_name, self.dt_resource_group) - ) - # TODO: Refactor - limited interface def assert_twin_attributes( diff --git a/pytest.ini.example b/pytest.ini.example index b89a4be91..ede7b5ff2 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -19,6 +19,7 @@ env = azext_iot_teststorageuri= azext_iot_teststorageid= azext_iot_central_app_id= + azext_dt_region= azext_dt_ep_eventgrid_topic= azext_dt_ep_servicebus_namespace= azext_dt_ep_servicebus_policy= From 3b07ed601c289087f0956b103a28b18a1466d1c4 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Thu, 28 Jan 2021 14:59:47 -0800 Subject: [PATCH 168/179] DT Help + test improvement. (#306) * DT Help + test improvement. * Update help example. --- azext_iot/digitaltwins/_help.py | 14 +++++++++++++- azext_iot/tests/digitaltwins/__init__.py | 4 ++-- .../test_dt_privatelinks_lifecycle_int.py | 11 +++++++++++ .../test_dt_resource_lifecycle_int.py | 16 ++++++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index 10dd5c1ae..a581f8d0c 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -19,7 +19,7 @@ def load_digitaltwins_help(): helps["dt create"] = """ type: command - short-summary: Create a new Digital Twins instance. + short-summary: Create or update a Digital Twins instance. examples: - name: Create instance in target resource group using the resource group location. @@ -50,6 +50,18 @@ def load_digitaltwins_help(): "/subscriptions/a12345ea-bb21-994d-2263-c716348e32a1/resourceGroups/ProResourceGroup/providers/Microsoft.EventHub/namespaces/myEventHubNamespace/eventhubs/myEventHub" "/subscriptions/a12345ea-bb21-994d-2263-c716348e32a1/resourceGroups/ProResourceGroup/providers/Microsoft.ServiceBus/namespaces/myServiceBusNamespace/topics/myTopic" --role MyCustomRole + + - name: Update an instance in the target resource group to enable system managed identity. + text: > + az dt create -n {instance_name} -g {resouce_group} --assign-identity + + - name: Update an instance in the target resource group to disable system managed identity. + text: > + az dt create -n {instance_name} -g {resouce_group} --assign-identity false + + - name: Update an instance in the target resource group with new tag values and disable public network access. + text: > + az dt create -n {instance_name} -g {resouce_group} --tags env=prod --public-network-access Disabled """ helps["dt show"] = """ diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py index 12f33c47a..6632777ff 100644 --- a/azext_iot/tests/digitaltwins/__init__.py +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -160,7 +160,7 @@ 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 = 5, interval: int = 3 + self, instance: dict, wait_in_sec: int = 5, interval: int = 4 ): from time import sleep @@ -176,7 +176,7 @@ def wait_for_hostname( ) ).as_json() - if refereshed_instance.get("hostName"): + if refereshed_instance.get("hostName") and refereshed_instance["provisioningState"] == "Succeeded": return refereshed_instance return 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 714adfc40..706919f40 100644 --- a/azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_privatelinks_lifecycle_int.py @@ -28,11 +28,22 @@ def test_dt_privatelinks(self): ) ).get_output_in_json() self.track_instance(create_output) + create_output = self.wait_for_hostname(create_output) # Fail test if hostName missing assert create_output.get( "hostName" ), "Service failed to provision DT instance: {}.".format(instance_name) + assert create_output["publicNetworkAccess"] == "Enabled" + + update_output = self.cmd( + "dt create -n {} -g {} -l {} --public-network-access Disabled".format( + instance_name, + self.rg, + self.region, + ) + ).get_output_in_json() + assert update_output["publicNetworkAccess"] == "Disabled" list_priv_links = self.cmd( "dt network private-link list -n {} -g {}".format( 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 0734fce04..f282a5cdb 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -130,6 +130,22 @@ def test_dt_resource(self): assign_identity=True, ) + # Update tags and disable MSI + updated_tags = "env=test tier=premium" + updated_tags_dict = {"env": "test", "tier": "premium"} + remove_msi_output = self.cmd( + "dt create -n {} -g {} --assign-identity false --tags {}".format(instance_names[1], self.rg, updated_tags) + ).get_output_in_json() + + assert_common_resource_attributes( + self.wait_for_hostname(remove_msi_output), + instance_names[1], + self.rg, + self.rg_region, + tags=updated_tags_dict, + assign_identity=False, + ) + list_output = self.cmd("dt list").get_output_in_json() filtered_list = filter_dt_list(list_output, instance_names) assert len(filtered_list) == len(instance_names) From f530a98728473a64002573f40b3fcb7a38102625 Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 1 Feb 2021 11:10:50 -0800 Subject: [PATCH 169/179] Assign MSI principal IT (#308) * Assign MSI principal test. * add skip test decorator --- .../test_dt_resource_lifecycle_int.py | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) 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 f282a5cdb..8dcc093a2 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -57,9 +57,36 @@ class TestDTResourceLifecycle(DTLiveScenarioTest): def __init__(self, test_case): super(TestDTResourceLifecycle, self).__init__(test_case) + @pytest.mark.skipif( + not all( + [ + settings.env.azext_dt_ep_rg, + settings.env.azext_dt_ep_eventgrid_topic, + settings.env.azext_dt_ep_servicebus_topic, + settings.env.azext_dt_ep_servicebus_namespace, + ] + ), + reason="Required env vars missing.", + ) def test_dt_resource(self): self.wait_for_capacity(capacity=3) + eventgrid_topic_id = self.cmd( + "eventgrid topic show -g {} -n {}".format( + settings.env.azext_dt_ep_rg, settings.env.azext_dt_ep_eventgrid_topic + ) + ).get_output_in_json()["id"] + + servicebus_topic_id = self.cmd( + "servicebus topic show -g {} -n {} --namespace-name {}".format( + settings.env.azext_dt_ep_rg, + settings.env.azext_dt_ep_servicebus_topic, + settings.env.azext_dt_ep_servicebus_namespace, + ) + ).get_output_in_json()["id"] + + scope_ids = [eventgrid_topic_id, servicebus_topic_id] + instance_names = [generate_resource_id(), generate_resource_id()] create_output = self.cmd( "dt create -n {} -g {} -l {} --tags {}".format( @@ -79,6 +106,18 @@ def test_dt_resource(self): MOCK_RESOURCE_TAGS_DICT, ) + show_output = self.cmd( + "dt show -n {}".format(instance_names[0]) + ).get_output_in_json() + + assert_common_resource_attributes( + show_output, + instance_names[0], + self.rg, + self.region, + MOCK_RESOURCE_TAGS_DICT, + ) + # Explictly assert create prevents provisioning on a name conflict (across regions) self.cmd( "dt create -n {} -g {} -l {} --tags {}".format( @@ -92,7 +131,9 @@ def test_dt_resource(self): # No location specified. Use the resource group location. create_msi_output = self.cmd( - "dt create -n {} -g {} --assign-identity".format(instance_names[1], self.rg) + "dt create -n {} -g {} --assign-identity --scopes {}".format( + instance_names[1], self.rg, " ".join(scope_ids) + ) ).get_output_in_json() self.track_instance(create_msi_output) @@ -105,18 +146,6 @@ def test_dt_resource(self): assign_identity=True, ) - show_output = self.cmd( - "dt show -n {}".format(instance_names[0]) - ).get_output_in_json() - - assert_common_resource_attributes( - show_output, - instance_names[0], - self.rg, - self.region, - MOCK_RESOURCE_TAGS_DICT, - ) - show_msi_output = self.cmd( "dt show -n {} -g {}".format(instance_names[1], self.rg) ).get_output_in_json() @@ -130,11 +159,27 @@ def test_dt_resource(self): assign_identity=True, ) + role_assignment_egt_list = self.cmd( + "role assignment list --scope {} --assignee {}".format( + eventgrid_topic_id, show_msi_output["identity"]["principalId"] + ) + ).get_output_in_json() + assert len(role_assignment_egt_list) == 1 + + role_assignment_sbt_list = self.cmd( + "role assignment list --scope {} --assignee {}".format( + servicebus_topic_id, show_msi_output["identity"]["principalId"] + ) + ).get_output_in_json() + assert len(role_assignment_sbt_list) == 1 + # Update tags and disable MSI updated_tags = "env=test tier=premium" updated_tags_dict = {"env": "test", "tier": "premium"} remove_msi_output = self.cmd( - "dt create -n {} -g {} --assign-identity false --tags {}".format(instance_names[1], self.rg, updated_tags) + "dt create -n {} -g {} --assign-identity false --tags {}".format( + instance_names[1], self.rg, updated_tags + ) ).get_output_in_json() assert_common_resource_attributes( @@ -633,10 +678,14 @@ def assert_common_endpoint_attributes( assert endpoint_output["properties"]["createdTime"] if dead_letter_secret: - assert endpoint_output["properties"]["deadLetterSecret"] + assert endpoint_output["properties"][ + "deadLetterSecret" + ], "Expected deadletter secret." if dead_letter_endpoint: - assert endpoint_output["properties"]["deadLetterUri"] + assert endpoint_output["properties"][ + "deadLetterUri" + ], "Expected deadletter Uri." # Currently DT -> EventGrid is only key based. if endpoint_type == ADTEndpointType.eventgridtopic: From 518a43d287e0804781c24a799736e3b85e1e01e4 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Mon, 1 Feb 2021 19:21:45 -0800 Subject: [PATCH 170/179] Pipeline testing improvements (#310) * Enable testing tasks to continue even if tests fail * Build status will be set to 'SucceededWithIssues' * Moved version check task to build stage * Fix version verification task not waiting for prereqs --- .azure-devops/create-release.yml | 25 +++++++++++++------------ .azure-devops/templates/run-tests.yml | 1 + 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.azure-devops/create-release.yml b/.azure-devops/create-release.yml index 572588f78..45d869c3b 100644 --- a/.azure-devops/create-release.yml +++ b/.azure-devops/create-release.yml @@ -39,6 +39,17 @@ stages: - 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 tests' pool: @@ -85,16 +96,6 @@ stages: runIntTests: 'false' runUnitTests: 'true' - - job: 'recordVersion' - displayName: 'Install and verify version' - steps: - - template: templates/setup-dev-test-env.yml - parameters: - pythonVersion: $(pythonVersion) - architecture: $(architecture) - - - template: templates/install-and-record-version.yml - - stage: 'kpi' displayName: 'Build KPIs' dependsOn: [build, test] @@ -119,8 +120,8 @@ stages: pool: vmImage: 'vs2017-win2016' variables: - CLIVersion: $[ stageDependencies.test.recordVersion.outputs['setupVersion.CLIVersion'] ] - ReleaseTitle: $[ stageDependencies.test.recordVersion.outputs['setupVersion.ReleaseTitle'] ] + CLIVersion: $[ stageDependencies.build.recordVersion.outputs['setupVersion.CLIVersion'] ] + ReleaseTitle: $[ stageDependencies.build.recordVersion.outputs['setupVersion.ReleaseTitle'] ] steps: - task: DownloadBuildArtifacts@0 diff --git a/.azure-devops/templates/run-tests.yml b/.azure-devops/templates/run-tests.yml index 58840281d..91db4af4f 100644 --- a/.azure-devops/templates/run-tests.yml +++ b/.azure-devops/templates/run-tests.yml @@ -18,6 +18,7 @@ steps: - ${{ if eq(parameters.runIntTests, 'true') }}: - task: AzureCLI@2 + continueOnError: true displayName: '${{ parameters.name }} integration tests' inputs: azureSubscription: AzIoTCLIService From 7a5f6764e807752ca31272a811750ade555b5d1f Mon Sep 17 00:00:00 2001 From: Paymaun Date: Wed, 3 Feb 2021 15:53:11 -0800 Subject: [PATCH 171/179] Improve test timing. (#312) --- azext_iot/tests/digitaltwins/__init__.py | 2 +- .../test_dt_model_lifecycle_int.py | 12 ++--- .../test_dt_resource_lifecycle_int.py | 10 ++-- .../test_dt_twin_lifecycle_int.py | 54 ++++++++----------- 4 files changed, 35 insertions(+), 43 deletions(-) diff --git a/azext_iot/tests/digitaltwins/__init__.py b/azext_iot/tests/digitaltwins/__init__.py index 6632777ff..e36950edb 100644 --- a/azext_iot/tests/digitaltwins/__init__.py +++ b/azext_iot/tests/digitaltwins/__init__.py @@ -160,7 +160,7 @@ 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 = 5, interval: int = 4 + self, instance: dict, wait_in_sec: int = 10, interval: int = 4 ): from time import sleep diff --git a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py index c4a60b93e..9fce3100f 100644 --- a/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_model_lifecycle_int.py @@ -32,13 +32,11 @@ def test_dt_models(self): component_dtmi = "dtmi:com:example:Thermostat;1" room_dtmi = "dtmi:com:example:Room;1" - self.track_instance( - self.cmd( - "dt create -n {} -g {} -l {}".format( - instance_name, self.rg, self.region - ) - ).get_output_in_json() - ) + create_output = self.cmd( + "dt create -n {} -g {} -l {}".format(instance_name, self.rg, self.region) + ).get_output_in_json() + self.track_instance(create_output) + self.wait_for_hostname(create_output) self.cmd( "dt role-assignment create -n {} -g {} --assignee {} --role '{}'".format( 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 8dcc093a2..48d50b0ea 100644 --- a/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_resource_lifecycle_int.py @@ -458,7 +458,7 @@ def test_dt_endpoints_routes(self): logger.debug("Adding key based eventhub endpoint...") add_ep_output = self.cmd( - "dt endpoint create eventhub -n {} --ehg {} --ehn {} --ehp {} --eh {} --ehs {} --en {} --dsu {}".format( + "dt endpoint create eventhub -n {} --ehg {} --ehn {} --ehp {} --eh {} --ehs {} --en {} --dsu '{}'".format( endpoints_instance_name, eventhub_rg, eventhub_namespace, @@ -678,9 +678,11 @@ def assert_common_endpoint_attributes( assert endpoint_output["properties"]["createdTime"] if dead_letter_secret: - assert endpoint_output["properties"][ - "deadLetterSecret" - ], "Expected deadletter secret." + pass + # TODO: This appears to be flaky. + # assert endpoint_output["properties"][ + # "deadLetterSecret" + # ], "Expected deadletter secret." if dead_letter_endpoint: assert endpoint_output["properties"][ 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 33280ef19..0c0d96875 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -31,13 +31,11 @@ def test_dt_twin(self): room_twin_id = "myroom" thermostat_component_id = "Thermostat" - self.track_instance( - self.cmd( - "dt create -n {} -g {} -l {}".format( - instance_name, self.rg, self.region - ) - ).get_output_in_json() - ) + create_output = self.cmd( + "dt create -n {} -g {} -l {}".format(instance_name, self.rg, self.region) + ).get_output_in_json() + self.track_instance(create_output) + self.wait_for_hostname(create_output) self.cmd( "dt role-assignment create -n {} -g {} --assignee {} --role '{}'".format( @@ -54,9 +52,7 @@ def test_dt_twin(self): ) twin_query_result = self.cmd( - "dt twin query -n {} -q 'select * from digitaltwins'".format( - instance_name - ) + "dt twin query -n {} -q 'select * from digitaltwins'".format(instance_name) ).get_output_in_json() assert len(twin_query_result["result"]) == 0 @@ -71,11 +67,7 @@ def test_dt_twin(self): ) self.kwargs["emptyThermostatComponentJson"] = json.dumps( - { - "Thermostat": { - "$metadata": {} - } - } + {"Thermostat": {"$metadata": {}}} ) floor_twin = self.cmd( @@ -95,12 +87,9 @@ def test_dt_twin(self): # create twin will fail without --properties self.cmd( "dt twin create -n {} -g {} --dtmi {} --twin-id {}".format( - instance_name, - self.rg, - room_dtmi, - room_twin_id + instance_name, self.rg, room_dtmi, room_twin_id ), - expect_failure=True + expect_failure=True, ) # minimum component object with empty $metadata object @@ -190,9 +179,7 @@ def test_dt_twin(self): "dt twin show -n {} --twin-id {} {}".format( instance_name, twin_tuple[0], - "-g {}".format(self.rg) - if twins_id_list[-1] == twin_tuple - else "", + "-g {}".format(self.rg) if twins_id_list[-1] == twin_tuple else "", ) ).get_output_in_json() assert_twin_attributes( @@ -205,7 +192,9 @@ def test_dt_twin(self): update_twin_result = self.cmd( "dt twin update -n {} --twin-id {} --json-patch '{}'".format( - instance_name, room_twin_id, "{temperatureJsonPatch}", + instance_name, + room_twin_id, + "{temperatureJsonPatch}", ) ).get_output_in_json() @@ -288,7 +277,8 @@ def test_dt_twin(self): twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} --twin-id {}".format( - instance_name, floor_twin_id, + instance_name, + floor_twin_id, ) ).get_output_in_json() assert len(twin_relationship_list_result) == 1 @@ -305,14 +295,16 @@ def test_dt_twin(self): twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} --twin-id {}".format( - instance_name, room_twin_id, + instance_name, + room_twin_id, ) ).get_output_in_json() assert len(twin_relationship_list_result) == 0 twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} --twin-id {} --incoming".format( - instance_name, room_twin_id, + instance_name, + room_twin_id, ) ).get_output_in_json() assert len(twin_relationship_list_result) == 1 @@ -327,7 +319,9 @@ def test_dt_twin(self): # No output from API for delete edge self.cmd( "dt twin relationship delete -n {} --twin-id {} -r {}".format( - instance_name, floor_twin_id, relationship_id, + instance_name, + floor_twin_id, + relationship_id, ) ) @@ -370,9 +364,7 @@ def test_dt_twin(self): "dt twin delete -n {} --twin-id {} {}".format( instance_name, twin_tuple[0], - "-g {}".format(self.rg) - if twins_id_list[-1] == twin_tuple - else "", + "-g {}".format(self.rg) if twins_id_list[-1] == twin_tuple else "", ) ) sleep(10) # Wait for API to catch up From 660f8d5ad8f081cf4e3db47e743c0cc32cfd526f Mon Sep 17 00:00:00 2001 From: vilit1 <73560279+vilit1@users.noreply.github.com> Date: Fri, 5 Feb 2021 09:18:05 -0800 Subject: [PATCH 172/179] Add Etags to dps and dt, unit tests (#307) * Update with linting * Add etag functionality to device twins * Update dps etags to no longer validate etag * Add tests to dt twin commands * linter fixes * fix if_none_match * Add tests to dt twin commands * Add data plane fixture * Add replace * Add dps tests, remove etag from create calls --- azext_iot/digitaltwins/commands_twins.py | 34 +- azext_iot/digitaltwins/params.py | 8 + azext_iot/digitaltwins/providers/twin.py | 122 +- azext_iot/operations/dps.py | 38 +- .../test_dt_twin_lifecycle_int.py | 38 + .../tests/digitaltwins/test_dt_twins_unit.py | 1255 +++++++++++++++++ azext_iot/tests/dps/test_iot_dps_unit.py | 32 +- 7 files changed, 1434 insertions(+), 93 deletions(-) create mode 100644 azext_iot/tests/digitaltwins/test_dt_twins_unit.py diff --git a/azext_iot/digitaltwins/commands_twins.py b/azext_iot/digitaltwins/commands_twins.py index c6f0132fd..fb4570753 100644 --- a/azext_iot/digitaltwins/commands_twins.py +++ b/azext_iot/digitaltwins/commands_twins.py @@ -18,11 +18,17 @@ def query_twins( def create_twin( - cmd, name_or_hostname, twin_id, model_id, properties=None, resource_group_name=None + cmd, + name_or_hostname, + twin_id, + model_id, + replace=False, + properties=None, + resource_group_name=None ): twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.create( - twin_id=twin_id, model_id=model_id, properties=properties + twin_id=twin_id, model_id=model_id, replace=replace, properties=properties ) @@ -31,14 +37,14 @@ def show_twin(cmd, name_or_hostname, twin_id, resource_group_name=None): return twin_provider.get(twin_id) -def update_twin(cmd, name_or_hostname, twin_id, json_patch, resource_group_name=None): +def update_twin(cmd, name_or_hostname, twin_id, json_patch, resource_group_name=None, etag=None): twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) - return twin_provider.update(twin_id=twin_id, json_patch=json_patch) + return twin_provider.update(twin_id=twin_id, json_patch=json_patch, etag=etag) -def delete_twin(cmd, name_or_hostname, twin_id, resource_group_name=None): +def delete_twin(cmd, name_or_hostname, twin_id, resource_group_name=None, etag=None): twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) - return twin_provider.delete(twin_id) + return twin_provider.delete(twin_id, etag=etag) def create_relationship( @@ -48,6 +54,7 @@ def create_relationship( target_twin_id, relationship_id, relationship, + replace=False, properties=None, resource_group_name=None, ): @@ -57,6 +64,7 @@ def create_relationship( target_twin_id=target_twin_id, relationship_id=relationship_id, relationship=relationship, + replace=replace, properties=properties, ) @@ -77,10 +85,11 @@ def update_relationship( relationship_id, json_patch, resource_group_name=None, + etag=None ): twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.update_relationship( - twin_id=twin_id, relationship_id=relationship_id, json_patch=json_patch, + twin_id=twin_id, relationship_id=relationship_id, json_patch=json_patch, etag=etag ) @@ -101,11 +110,11 @@ def list_relationships( def delete_relationship( - cmd, name_or_hostname, twin_id, relationship_id, resource_group_name=None, + cmd, name_or_hostname, twin_id, relationship_id, resource_group_name=None, etag=None ): twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.delete_relationship( - twin_id=twin_id, relationship_id=relationship_id + twin_id=twin_id, relationship_id=relationship_id, etag=etag ) @@ -132,10 +141,9 @@ def show_component( def update_component( - cmd, name_or_hostname, twin_id, component_path, json_patch, resource_group_name=None + cmd, name_or_hostname, twin_id, component_path, json_patch, resource_group_name=None, etag=None ): twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) - twin_provider.update_component( - twin_id=twin_id, component_path=component_path, json_patch=json_patch + return twin_provider.update_component( + twin_id=twin_id, component_path=component_path, json_patch=json_patch, etag=etag ) - return diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index f9a217107..e54ca7c11 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -278,11 +278,19 @@ def load_digitaltwins_arguments(self, _): "Updates to property values and $model elements may happen in the same request. " "Operations are limited to add, replace and remove. Provide file path or inline JSON.", ) + context.argument( + "etag", options_list=["--etag", "-e"], help="Entity tag value." + ) context.argument( "component_path", options_list=["--component"], help="The path to the DTDL component.", ) + context.argument( + "replace", + options_list=["--replace"], + help="Indicates the operation should replace an existing twin if it exists." + ) with self.argument_context("dt twin create") as context: context.argument( diff --git a/azext_iot/digitaltwins/providers/twin.py b/azext_iot/digitaltwins/providers/twin.py index b9f64c96f..5dada72d8 100644 --- a/azext_iot/digitaltwins/providers/twin.py +++ b/azext_iot/digitaltwins/providers/twin.py @@ -17,6 +17,14 @@ logger = get_logger(__name__) +class TwinOptions(): + def __init__(self, if_match=None, if_none_match=None): + self.if_match = if_match + self.if_none_match = if_none_match + self.traceparent = None + self.tracestate = None + + class TwinProvider(DigitalTwinsProvider): def __init__(self, cmd, name, rg=None): super(TwinProvider, self).__init__( @@ -31,13 +39,16 @@ def __init__(self, cmd, name, rg=None): def invoke_query(self, query, show_cost): from azext_iot.digitaltwins.providers.generic import accumulate_result - accumulated_result, cost = accumulate_result( - self.query_sdk.query_twins, - values_name="value", - token_name="continuationToken", - token_arg_name="continuation_token", - query=query, - ) + try: + accumulated_result, cost = accumulate_result( + self.query_sdk.query_twins, + values_name="value", + token_name="continuationToken", + token_arg_name="continuation_token", + query=query, + ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) query_result = {} query_result["result"] = accumulated_result @@ -46,11 +57,10 @@ def invoke_query(self, query, show_cost): return query_result - def create(self, twin_id, model_id, properties=None): - target_model = self.model_provider.get(id=model_id) + def create(self, twin_id, model_id, replace=False, properties=None): twin_request = { "$dtId": twin_id, - "$metadata": {"$model": target_model["id"]}, + "$metadata": {"$model": model_id}, } if properties: @@ -62,7 +72,8 @@ def create(self, twin_id, model_id, properties=None): logger.info("Twin payload %s", json.dumps(twin_request)) try: - return self.twins_sdk.add(id=twin_id, twin=twin_request, if_none_match="*") + options = TwinOptions(if_none_match=(None if replace else "*")) + return self.twins_sdk.add(id=twin_id, twin=twin_request, digital_twins_add_options=options) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) @@ -72,7 +83,7 @@ def get(self, twin_id): except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) - def update(self, twin_id, json_patch): + def update(self, twin_id, json_patch, etag=None): json_patch = process_json_arg(content=json_patch, argument_name="json-patch") json_patch_collection = [] @@ -84,17 +95,19 @@ def update(self, twin_id, json_patch): logger.info("Patch payload %s", json.dumps(json_patch_collection)) try: + options = TwinOptions(if_match=(etag if etag else "*")) self.twins_sdk.update( - id=twin_id, patch_document=json_patch_collection, if_match="*", raw=True + id=twin_id, patch_document=json_patch_collection, digital_twins_update_options=options, raw=True ) return self.get(twin_id=twin_id) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) - def delete(self, twin_id): + def delete(self, twin_id, etag=None): # Not a json response try: - self.twins_sdk.delete(id=twin_id, if_match="*", raw=True) + options = TwinOptions(if_match=(etag if etag else "*")) + self.twins_sdk.delete(id=twin_id, digital_twins_delete_options=options, raw=True) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) @@ -104,6 +117,7 @@ def add_relationship( target_twin_id, relationship_id, relationship, + replace=False, properties=None, ): relationship_request = { @@ -118,18 +132,25 @@ def add_relationship( relationship_request.update(properties) logger.info("Relationship payload %s", json.dumps(relationship_request)) - return self.twins_sdk.add_relationship( - id=twin_id, - relationship_id=relationship_id, - relationship=relationship_request, - if_none_match="*", - raw=True, - ).response.json() + try: + options = TwinOptions(if_none_match=(None if replace else "*")) + return self.twins_sdk.add_relationship( + id=twin_id, + relationship_id=relationship_id, + relationship=relationship_request, + digital_twins_add_relationship_options=options, + raw=True, + ).response.json() + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) def get_relationship(self, twin_id, relationship_id): - return self.twins_sdk.get_relationship_by_id( - id=twin_id, relationship_id=relationship_id, raw=True - ).response.json() + try: + return self.twins_sdk.get_relationship_by_id( + id=twin_id, relationship_id=relationship_id, raw=True + ).response.json() + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) def list_relationships( self, twin_id, incoming_relationships=False, relationship=None @@ -147,6 +168,8 @@ def list_relationships( incoming_result.extend(incoming_pager.advance_page()) except StopIteration: pass + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) if relationship: incoming_result = [ @@ -157,7 +180,7 @@ def list_relationships( return incoming_result - def update_relationship(self, twin_id, relationship_id, json_patch): + def update_relationship(self, twin_id, relationship_id, json_patch, etag=None): json_patch = process_json_arg(content=json_patch, argument_name="json-patch") json_patch_collection = [] @@ -169,11 +192,12 @@ def update_relationship(self, twin_id, relationship_id, json_patch): logger.info("Patch payload %s", json.dumps(json_patch_collection)) try: + options = TwinOptions(if_match=(etag if etag else "*")) self.twins_sdk.update_relationship( id=twin_id, relationship_id=relationship_id, patch_document=json_patch_collection, - if_match="*", + digital_twins_update_relationship_options=options, ) return self.get_relationship( twin_id=twin_id, relationship_id=relationship_id @@ -181,20 +205,24 @@ def update_relationship(self, twin_id, relationship_id, json_patch): except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) - def delete_relationship(self, twin_id, relationship_id): + def delete_relationship(self, twin_id, relationship_id, etag=None): try: + options = TwinOptions(if_match=(etag if etag else "*")) self.twins_sdk.delete_relationship( - id=twin_id, relationship_id=relationship_id, if_match="*" + id=twin_id, relationship_id=relationship_id, digital_twins_delete_relationship_options=options ) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) def get_component(self, twin_id, component_path): - return self.twins_sdk.get_component( - id=twin_id, component_path=component_path, raw=True - ).response.json() + try: + return self.twins_sdk.get_component( + id=twin_id, component_path=component_path, raw=True + ).response.json() + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) - def update_component(self, twin_id, component_path, json_patch): + def update_component(self, twin_id, component_path, json_patch, etag=None): json_patch = process_json_arg(content=json_patch, argument_name="json-patch") json_patch_collection = [] @@ -206,13 +234,14 @@ def update_component(self, twin_id, component_path, json_patch): logger.info("Patch payload %s", json.dumps(json_patch_collection)) try: - # TODO: API does not return response + options = TwinOptions(if_match=(etag if etag else "*")) self.twins_sdk.update_component( id=twin_id, component_path=component_path, patch_document=json_patch_collection, - if_match="*", + digital_twins_update_component_options=options, ) + return self.get_component(twin_id=twin_id, component_path=component_path) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) @@ -236,18 +265,21 @@ def send_telemetry(self, twin_id, telemetry=None, dt_id=None, component_path=Non if not dt_id: dt_id = str(uuid4()) - if component_path: - self.twins_sdk.send_component_telemetry( + try: + if component_path: + self.twins_sdk.send_component_telemetry( + id=twin_id, + message_id=dt_id, + dt_timestamp=dt_timestamp, + component_path=component_path, + telemetry=telemetry_request, + ) + + self.twins_sdk.send_telemetry( id=twin_id, message_id=dt_id, dt_timestamp=dt_timestamp, - component_path=component_path, telemetry=telemetry_request, ) - - self.twins_sdk.send_telemetry( - id=twin_id, - message_id=dt_id, - dt_timestamp=dt_timestamp, - telemetry=telemetry_request, - ) + except ErrorResponseException as e: + raise CLIError(unpack_msrest_error(e)) diff --git a/azext_iot/operations/dps.py b/azext_iot/operations/dps.py index a9962c9a6..91fe91e89 100644 --- a/azext_iot/operations/dps.py +++ b/azext_iot/operations/dps.py @@ -110,7 +110,7 @@ def iot_dps_device_enrollment_create( iot_hubs=None, edge_enabled=False, webhook_url=None, - api_version=None, + api_version=None ): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: @@ -197,17 +197,8 @@ def iot_dps_device_enrollment_update( try: resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - enrollment_record = sdk.get_individual_enrollment(enrollment_id) - # Verify etag - if ( - etag - and hasattr(enrollment_record, "etag") - and etag != enrollment_record.etag.replace('"', "") - ): - raise LookupError("enrollment etag doesn't match.") - if not etag: - etag = enrollment_record.etag.replace('"', "") + # Verify and update attestation information attestation_type = enrollment_record.attestation.type _validate_arguments_for_attestation_mechanism( @@ -274,21 +265,21 @@ def iot_dps_device_enrollment_update( enrollment_record.capabilities = DeviceCapabilities(iot_edge=edge_enabled) return sdk.create_or_update_individual_enrollment( - enrollment_id, enrollment_record, etag + enrollment_id, enrollment_record, if_match=(etag if etag else "*") ) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) def iot_dps_device_enrollment_delete( - client, enrollment_id, dps_name, resource_group_name + client, enrollment_id, dps_name, resource_group_name, etag=None ): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.delete_individual_enrollment(enrollment_id) + return sdk.delete_individual_enrollment(enrollment_id, if_match=(etag if etag else "*")) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -458,15 +449,6 @@ def iot_dps_device_enrollment_group_update( sdk = resolver.get_sdk(SdkType.dps_sdk) enrollment_record = sdk.get_enrollment_group(enrollment_id) - # Verify etag - if ( - etag - and hasattr(enrollment_record, "etag") - and etag != enrollment_record.etag.replace('"', "") - ): - raise LookupError("enrollment etag doesn't match.") - if not etag: - etag = enrollment_record.etag.replace('"', "") # Update enrollment information if enrollment_record.attestation.type == AttestationType.symmetricKey.value: enrollment_record.attestation = sdk.get_enrollment_group_attestation_mechanism( @@ -552,21 +534,21 @@ def iot_dps_device_enrollment_group_update( if edge_enabled is not None: enrollment_record.capabilities = DeviceCapabilities(iot_edge=edge_enabled) return sdk.create_or_update_enrollment_group( - enrollment_id, enrollment_record, etag + enrollment_id, enrollment_record, if_match=(etag if etag else "*") ) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) def iot_dps_device_enrollment_group_delete( - client, enrollment_id, dps_name, resource_group_name + client, enrollment_id, dps_name, resource_group_name, etag=None ): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.delete_enrollment_group(enrollment_id) + return sdk.delete_enrollment_group(enrollment_id, if_match=(etag if etag else "*")) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) @@ -608,13 +590,13 @@ def iot_dps_registration_get(client, dps_name, resource_group_name, registration raise CLIError(e) -def iot_dps_registration_delete(client, dps_name, resource_group_name, registration_id): +def iot_dps_registration_delete(client, dps_name, resource_group_name, registration_id, etag=None): target = get_iot_dps_connection_string(client, dps_name, resource_group_name) try: resolver = SdkResolver(target=target) sdk = resolver.get_sdk(SdkType.dps_sdk) - return sdk.delete_device_registration_state(registration_id) + return sdk.delete_device_registration_state(registration_id, if_match=(etag if etag else "*")) except ProvisioningServiceErrorDetailsException as e: raise CLIError(e) 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 0c0d96875..defe13cf9 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -111,6 +111,44 @@ def test_dt_twin(self): component_name=thermostat_component_id, ) + replaced_room_twin = self.cmd( + "dt twin create -n {} -g {} --dtmi {} --twin-id {} --replace --properties '{}'".format( + instance_name, + self.rg, + room_dtmi, + room_twin_id, + "{tempAndThermostatComponentJson}", + ) + ).get_output_in_json() + + assert_twin_attributes( + twin=replaced_room_twin, + expected_twin_id=room_twin_id, + expected_dtmi=room_dtmi, + properties=self.kwargs["tempAndThermostatComponentJson"], + component_name=thermostat_component_id, + ) + + # new twin cannot be created with same twin_id if replace not provided + self.cmd( + "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( + instance_name, + self.rg, + room_dtmi, + room_twin_id, + "{emptyThermostatComponentJson}", + ), + expect_failure=True + ) + + self.cmd( + "dt twin delete -n {} -g {} --twin-id {}".format( + instance_name, + self.rg, + room_twin_id, + ) + ) + room_twin = self.cmd( "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( instance_name, diff --git a/azext_iot/tests/digitaltwins/test_dt_twins_unit.py b/azext_iot/tests/digitaltwins/test_dt_twins_unit.py new file mode 100644 index 000000000..e337f3b67 --- /dev/null +++ b/azext_iot/tests/digitaltwins/test_dt_twins_unit.py @@ -0,0 +1,1255 @@ +# 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.digitaltwins import commands_twins as subject +from azext_iot.tests.generators import generate_generic_id +from msrest.paging import Paged + +instance_name = generate_generic_id() +hostname = "{}.subdomain.domain".format(instance_name) +etag = 'AAAA==' +resource_group = 'myrg' + +model_id = generate_generic_id() +twin_id = generate_generic_id() +target_twin_id = generate_generic_id() +relationship_id = generate_generic_id() +component_path = generate_generic_id() + +generic_result = json.dumps({"result": generate_generic_id()}) +generic_query = "select * from digitaltwins" +models_result = json.dumps({"id": model_id}) +generic_patch_1 = json.dumps({"a" : "b"}) +generic_patch_2 = json.dumps({"a" : "b", "c" : "d"}) + + +def generate_twin_result(twin_id=twin_id, etag=etag, model_id=model_id): + return json.dumps({ + "$dtId": twin_id, + "$etag": etag, + "$metadata": { + "$model": model_id + } + }) + + +def create_relationship(relationship_name=None): + return { + "$relationshipId": generate_generic_id(), + "relationship_name": relationship_name + } + + +@pytest.fixture +def control_and_data_plane_client(mocker, fixture_cmd): + from azext_iot.sdk.digitaltwins.controlplane import AzureDigitalTwinsManagementClient + from azext_iot.sdk.digitaltwins.dataplane import AzureDigitalTwinsAPI + from azext_iot.digitaltwins.providers.auth import DigitalTwinAuthentication + + 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"), + ) + + control_plane_patch = mocker.patch( + "azext_iot.digitaltwins.providers.digitaltwins_service_factory" + ) + control_plane_patch.return_value = AzureDigitalTwinsManagementClient( + credentials=DigitalTwinAuthentication( + fixture_cmd, "00000000-0000-0000-0000-000000000000" + ), + subscription_id="00000000-0000-0000-0000-000000000000", + ) + + data_plane_patch = mocker.patch( + "azext_iot.digitaltwins.providers.base.DigitalTwinsProvider.get_sdk" + ) + + data_plane_patch.return_value = AzureDigitalTwinsAPI( + credentials=DigitalTwinAuthentication( + fixture_cmd, "00000000-0000-0000-0000-000000000000" + ), + base_url="https://{}/".format(hostname) + ) + + return control_plane_patch, data_plane_patch + + +@pytest.fixture +def start_twin_response(mocked_response, control_and_data_plane_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.DigitalTwins/digitalTwinsInstances" + ), + status=200, + match_querystring=False, + body=json.dumps({"hostName": hostname}), + ) + + yield mocked_response + + +def check_resource_group_name_call(service_client, resource_group_input): + if ("management.azure.com/subscriptions" in service_client.calls[0].request.url): + return 1 + return 0 + + +class TestTwinQueryTwins(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + yield mocked_response + + @pytest.mark.parametrize( + "query_command, show_cost, servresult, numresultsperpage", + [ + (generic_query, True, [], 1), + (generic_query, False, [], 1), + (generic_query, True, [generate_twin_result()], 1), + (generic_query, False, [generate_twin_result()], 1), + (generic_query, True, [generate_twin_result(), generate_twin_result()], 2), + (generic_query, True, [generate_twin_result(), generate_twin_result(), generate_twin_result()], 2) + ] + ) + def test_query_twins( + self, fixture_cmd, service_client, query_command, show_cost, servresult, numresultsperpage + ): + # Set up number of pages, setting it to 1 if result is [] + numpages = int(len(servresult) / numresultsperpage) + if numpages == 0: + numpages += 1 + cost = 0 + + # Create and add the mocked responses + for i in range(numpages): + if i == 0: + url = "https://{}/query".format(hostname) + else: + url = "https://{}/query?{}".format(hostname, str(i)) + + if numpages - i == 1: + contToken = None + value = servresult[i * numresultsperpage:] + else: + contToken = "https://{}/query?{}".format(hostname, str(i + 1)) + value = servresult[i * numresultsperpage:(i + 1) * numresultsperpage] + + cost += 0.5 + i + service_client.add( + method=responses.POST, + url=url, + body=json.dumps({ + "value" : value, + "continuationToken" : contToken + }), + status=200, + content_type="application/json", + match_querystring=False, + headers={ + "Query-Charge": str(0.5 + i) + } + ) + + # Create expected result + query_result = { + "result": servresult + } + if show_cost: + query_result["cost"] = cost + + result = subject.query_twins( + cmd=fixture_cmd, + name_or_hostname=hostname, + query_command=query_command, + show_cost=show_cost, + resource_group_name=None + ) + + assert result == query_result + + @pytest.fixture(params=[(400, 200), (401, 200), (500, 200), (200, 400), (200, 401), (200, 500)]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.POST, + url="https://{}/query".format( + hostname + ), + body=json.dumps({ + "value": [generate_twin_result()], + "continuationToken": "https://{}/query?2".format(hostname) + }), + status=request.param[0], + content_type="application/json", + match_querystring=False, + headers={ + "Query-Charge": "1.0" + } + ) + + mocked_response.add( + method=responses.POST, + url="https://{}/query?2".format( + hostname + ), + body=json.dumps({"value": [generate_twin_result()]}), + status=request.param[1], + content_type="application/json", + match_querystring=False, + headers={ + "Query-Charge": "1.0" + } + ) + + yield mocked_response + + def test_list_relationship_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.query_twins( + cmd=fixture_cmd, + name_or_hostname=hostname, + query_command=generic_query, + show_cost=False, + resource_group_name=None + ) + + +class TestTwinCreateTwin(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.PUT, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generic_result, + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "replace, properties, resource_group_name", + [ + (False, None, None), + (True, None, None), + (False, None, resource_group), + (False, generic_patch_1, None), + (False, generic_patch_2, None) + ] + ) + def test_create_twin(self, fixture_cmd, service_client, replace, properties, resource_group_name): + result = subject.create_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + model_id=model_id, + replace=replace, + properties=properties, + resource_group_name=resource_group_name + ) + + start = check_resource_group_name_call(service_client, resource_group_name) + + twin_request = service_client.calls[start].request + assert twin_request.method == "PUT" + assert "{}/digitaltwins/{}".format(hostname, twin_id) in twin_request.url + + twin_request_body = json.loads(twin_request.body) + assert twin_request_body["$dtId"] == twin_id + assert twin_request_body["$metadata"]["$model"] == model_id + + if not replace: + assert twin_request.headers["If-None-Match"] == "*" + + if properties: + for (key, value) in json.loads(properties).items(): + assert twin_request_body[key] == value + + assert result == json.loads(generic_result) + + @pytest.fixture(params=[400, 401, 500]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.PUT, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generic_result, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_create_twin_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.create_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + model_id=model_id, + properties=None + ) + + +class TestTwinShowTwin(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generic_result, + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "resource_group_name", + [None, resource_group] + ) + def test_show_twin(self, fixture_cmd, service_client, resource_group_name): + result = subject.show_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + resource_group_name=resource_group_name + ) + + assert result == json.loads(generic_result) + + @pytest.fixture(params=[400, 401, 500]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generic_result, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "resource_group_name", + [None, resource_group] + ) + def test_show_twin_error(self, fixture_cmd, service_client_error, resource_group_name): + with pytest.raises(CLIError): + subject.show_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + resource_group_name=resource_group_name + ) + + +class TestTwinUpdateTwin(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.PATCH, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generic_result, + status=202, + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generate_twin_result(), + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "json_patch, resource_group_name, etag", + [ + (json.dumps({}), None, None), + (json.dumps({}), resource_group, None), + (json.dumps({}), None, etag), + (generic_patch_1, None, None), + (generic_patch_2, None, None) + ] + ) + def test_update_twin(self, fixture_cmd, service_client, json_patch, resource_group_name, etag): + result = subject.update_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + json_patch=json_patch, + resource_group_name=resource_group_name, + etag=etag + ) + + start = check_resource_group_name_call(service_client, resource_group_name) + + # check patch request + patch_request = service_client.calls[start].request + assert patch_request.method == "PATCH" + + expected_request_body = [json.loads(json_patch)] + assert json.loads(patch_request.body) == expected_request_body + + assert patch_request.headers["If-Match"] == etag if etag else "*" + + # check get request + get_request = service_client.calls[start + 1].request + assert get_request.method == "GET" + + assert result == json.loads(generate_twin_result()) + + @pytest.fixture(params=[(400, 200), (401, 200), (500, 200), (202, 400), (202, 401), (202, 500)]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.PATCH, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generic_result, + status=request.param[0], + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generic_result, + status=request.param[1], + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_update_twin_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.update_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + json_patch=generic_patch_1, + resource_group_name=None, + etag=None + ) + + +class TestTwinDeleteTwin(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.DELETE, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generic_result, + status=204, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "resource_group_name, etag", + [(None, None), (resource_group, None), (None, etag)] + ) + def test_delete_twin(self, fixture_cmd, service_client, resource_group_name, etag): + result = subject.delete_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + resource_group_name=resource_group_name, + etag=etag + ) + + start = check_resource_group_name_call(service_client, resource_group_name) + + delete_request = service_client.calls[start].request + assert delete_request.method == "DELETE" + assert delete_request.headers["If-Match"] == etag if etag else "*" + + assert result is None + + @pytest.fixture(params=[400, 401, 500]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.DELETE, + url="https://{}/digitaltwins/{}".format( + hostname, twin_id + ), + body=generic_result, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "resource_group_name, etag", + [(None, None), (resource_group, None), (None, etag)] + ) + def test_delete_twin_error(self, fixture_cmd, service_client_error, resource_group_name, etag): + with pytest.raises(CLIError): + subject.delete_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + resource_group_name=resource_group_name, + etag=etag + ) + + +class TestTwinCreateRelationship(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.PUT, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "relationship, replace, properties, resource_group_name", + [ + ("contains", False, None, None), + ("", False, None, None), + ("contains", True, None, None), + ("contains", False, generic_patch_1, None), + ("contains", False, generic_patch_2, None), + ("contains", False, None, resource_group) + ] + ) + def test_create_relationship(self, fixture_cmd, service_client, relationship, replace, properties, resource_group_name): + result = subject.create_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + target_twin_id=target_twin_id, + relationship_id=relationship_id, + relationship=relationship, + replace=replace, + properties=properties, + resource_group_name=resource_group_name + ) + + start = check_resource_group_name_call(service_client, resource_group_name) + + # check body + put_request = service_client.calls[start].request + assert put_request.method == "PUT" + assert "{}/digitaltwins/{}/relationships/{}".format(hostname, twin_id, relationship_id) in put_request.url + result_request_body = json.loads(put_request.body) + + assert result_request_body["$targetId"] == target_twin_id + assert result_request_body["$relationshipName"] == relationship + + if not replace: + assert put_request.headers["If-None-Match"] == "*" + + if properties: + for (key, value) in json.loads(properties).items(): + assert result_request_body[key] == value + + assert result == json.loads(generic_result) + + @pytest.fixture(params=[400, 401, 500]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.PUT, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_create_relationship_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.create_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + target_twin_id=target_twin_id, + relationship_id=relationship_id, + relationship="" + ) + + +class TestTwinShowRelationship(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize("resource_group_name", [None, resource_group]) + def test_show_relationship(self, fixture_cmd, service_client, resource_group_name): + result = subject.show_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + relationship_id=relationship_id, + resource_group_name=resource_group_name + ) + + assert result == json.loads(generic_result) + + @pytest.fixture(params=[400, 401, 500]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_show_relationship_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.show_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + relationship_id=relationship_id, + resource_group_name=None + ) + + +class TestTwinUpdateRelationship(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=200, + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.PATCH, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=204, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "json_patch, resource_group_name, etag", + [ + (json.dumps({}), None, None), + (generic_patch_1, None, None), + (generic_patch_1, resource_group, None), + (generic_patch_1, None, etag), + (generic_patch_2, None, None) + ] + ) + def test_update_relationship(self, fixture_cmd, service_client, json_patch, resource_group_name, etag): + result = subject.update_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + relationship_id=relationship_id, + json_patch=json_patch, + resource_group_name=resource_group_name, + etag=etag + ) + + start = check_resource_group_name_call(service_client, resource_group_name) + # check patch request + patch_request = service_client.calls[start].request + assert patch_request.method == "PATCH" + + expected_request_body = [json.loads(json_patch)] + assert json.loads(patch_request.body) == expected_request_body + + assert patch_request.headers["If-Match"] == etag if etag else "*" + + # check get request + get_request = service_client.calls[start + 1].request + assert get_request.method == "GET" + + assert result == json.loads(generic_result) + + @pytest.fixture(params=[(400, 200), (401, 200), (500, 200), (204, 400), (204, 401), (204, 500)]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=request.param[0], + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.PATCH, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=request.param[1], + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_update_relationship_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.update_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + relationship_id=relationship_id, + json_patch=generic_patch_1, + resource_group_name=None, + etag=None + ) + + +class TestTwinListRelationship(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + yield mocked_response + + @pytest.mark.parametrize( + "incoming_relationships, relationship, resource_group_name, servresult, numresultsperpage", + [ + (False, None, None, [], 1), + (True, None, None, [], 1), + (False, "", None, [], 1), + (True, "", None, [], 1), + (False, "", None, [create_relationship("")], 1), + (False, "contains", None, [], 1), + (True, "contains", None, [], 1), + (False, "contains", None, [create_relationship("contains")], 1), + (True, "contains", None, [create_relationship("contains")], 1), + (False, "contains", None, [create_relationship("other")], 1), + (True, "contains", None, [create_relationship("other")], 2), + (False, "contains", None, [create_relationship("other"), + create_relationship("contains"), + create_relationship("contains"), + create_relationship("other")], 2), + (True, "contains", None, [create_relationship("other"), + create_relationship("contains"), + create_relationship("contains"), + create_relationship("other")], 1), + (False, None, resource_group, [], 1) + ] + ) + def test_list_relationship( + self, + fixture_cmd, + service_client, + incoming_relationships, + relationship, + resource_group_name, + servresult, + numresultsperpage + ): + # Set up number of pages, setting it to 1 if result is [] + numpages = int(len(servresult) / numresultsperpage) + if numpages == 0: + numpages += 1 + relationship = "incomingrelationships" if incoming_relationships else "relationships" + + # Create and add the mocked responses + for i in range(numpages): + if i == 0: + url = "https://{}/digitaltwins/{}/{}".format(hostname, twin_id, relationship) + else: + url = "https://{}/digitaltwins/{}/{}?{}".format(hostname, twin_id, relationship, str(i)) + + if numpages - i == 1: + contToken = None + value = servresult[i * numresultsperpage:] + else: + contToken = "https://{}/digitaltwins/{}/{}?{}".format(hostname, twin_id, relationship, str(i + 1)) + value = servresult[i * numresultsperpage:(i + 1) * numresultsperpage] + + service_client.add( + method=responses.GET, + url=url, + body=json.dumps({ + "value" : value, + "nextLink" : contToken + }), + status=200, + content_type="application/json", + match_querystring=False + ) + + result = subject.list_relationships( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + incoming_relationships=incoming_relationships, + relationship=relationship, + resource_group_name=resource_group_name + ) + + # Check result, unpack if it is a Paged object + if incoming_relationships: + expected_result = [ + x + for x in servresult + if x["relationship_name"] and x["relationship_name"] == relationship + ] + assert result == expected_result + else: + assert isinstance(result, Paged) + unpacked_result = [] + try: + while True: + unpacked_result.extend(result.advance_page()) + except StopIteration: + pass + + assert unpacked_result == servresult + + @pytest.fixture(params=[(400, 200), (401, 200), (500, 200), (200, 400), (200, 401), (200, 500)]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/incomingrelationships".format( + hostname, twin_id + ), + body=json.dumps({ + "value": [create_relationship("contains")], + "nextLink": "https://{}/digitaltwins/{}/incomingrelationships?2".format(hostname, twin_id) + }), + status=request.param[0], + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/incomingrelationships?2".format( + hostname, twin_id + ), + body=json.dumps({"value": [create_relationship("contains")]}), + status=request.param[1], + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_list_relationship_error(self, fixture_cmd, service_client_error, ): + with pytest.raises(CLIError): + subject.list_relationships( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + incoming_relationships=True, + relationship=None, + resource_group_name=None + ) + + +class TestTwinDeleteRelationship(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.DELETE, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=204, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "resource_group_name, etag", + [(None, None), (resource_group, None), (None, etag)] + ) + def test_delete_relationship(self, fixture_cmd, service_client, resource_group_name, etag): + result = subject.delete_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + relationship_id=relationship_id, + resource_group_name=resource_group_name, + etag=etag + ) + + start = check_resource_group_name_call(service_client, resource_group_name) + + delete_request = service_client.calls[start].request + assert delete_request.method == "DELETE" + assert delete_request.headers["If-Match"] == etag if etag else "*" + + assert result is None + + @pytest.fixture(params=[400, 401, 500]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.DELETE, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship_id + ), + body=generic_result, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_delete_relationship_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.delete_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + relationship_id=relationship_id, + resource_group_name=None, + etag=None + ) + + +class TestTwinSendTelemetry(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.POST, + url="https://{}/digitaltwins/{}/telemetry".format( + hostname, twin_id + ), + body=generic_result, + status=204, + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.POST, + url="https://{}/digitaltwins/{}/components/{}/telemetry".format( + hostname, twin_id, component_path + ), + body=generic_result, + status=204, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "dt_id, component_path, telemetry, resource_group_name", + [ + (None, None, json.dumps({}), None), + ("DT_ID", None, json.dumps({}), None), + (None, component_path, json.dumps({}), None), + (None, None, generic_patch_1, None), + (None, None, generic_patch_2, None), + (None, None, json.dumps({}), resource_group) + ] + ) + def test_send_telemetry(self, fixture_cmd, service_client, dt_id, component_path, telemetry, resource_group_name): + result = subject.send_telemetry( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + dt_id=dt_id, + component_path=component_path, + telemetry=telemetry, + resource_group_name=resource_group_name, + ) + + start = check_resource_group_name_call(service_client, resource_group_name) + + if component_path: + component_telemetry_request = service_client.calls[start].request + assert component_telemetry_request.method == "POST" + assert ( + "{}/digitaltwins/{}/components/{}/telemetry".format(hostname, twin_id, component_path) + in component_telemetry_request.url + ) + + expected_request_body = json.loads(telemetry) + assert json.loads(component_telemetry_request.body) == expected_request_body + + if dt_id: + component_telemetry_request.headers["Message-Id"] == dt_id + + start += 1 + + # Check POST telemetry + twin_telemetry_request = service_client.calls[start].request + assert twin_telemetry_request.method == "POST" + assert "{}/digitaltwins/{}/telemetry".format(hostname, twin_id) in twin_telemetry_request.url + + expected_request_body = json.loads(telemetry) + assert json.loads(twin_telemetry_request.body) == expected_request_body + + if dt_id: + twin_telemetry_request.headers["Message-Id"] == dt_id + + assert result is None + + @pytest.fixture(params=[(400, 204), (401, 204), (500, 204), (204, 400), (204, 401), (204, 500)]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.POST, + url="https://{}/digitaltwins/{}/telemetry".format( + hostname, twin_id + ), + body=generic_result, + status=request.param[0], + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.POST, + url="https://{}/digitaltwins/{}/components/{}/telemetry".format( + hostname, twin_id, component_path + ), + body=generic_result, + status=request.param[1], + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_send_telemetry_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.send_telemetry( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + dt_id=None, + component_path=component_path, + telemetry=json.dumps({}), + resource_group_name=None, + ) + + +class TestTwinShowComponent(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/components/{}".format( + hostname, twin_id, component_path + ), + body=generic_result, + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize("resource_group_name", [None, resource_group]) + def test_show_component(self, fixture_cmd, service_client, resource_group_name): + result = subject.show_component( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + component_path=component_path, + resource_group_name=resource_group_name, + ) + + assert result == json.loads(generic_result) + + @pytest.fixture(params=[400, 401, 500]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/components/{}".format( + hostname, twin_id, component_path + ), + body=generic_result, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_show_component_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.show_component( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + component_path=component_path, + resource_group_name=None, + ) + + +class TestTwinUpdateComponent(object): + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + mocked_response.add( + method=responses.PATCH, + url="https://{}/digitaltwins/{}/components/{}".format( + hostname, twin_id, component_path + ), + body=generic_result, + status=204, + content_type="application/json", + match_querystring=False, + ) + + mocked_response.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/components/{}".format( + hostname, twin_id, component_path + ), + body=generic_result, + status=200, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + @pytest.mark.parametrize( + "json_patch, resource_group_name, etag", + [ + (json.dumps({}), None, None), + (generic_patch_1, None, None), + (generic_patch_2, None, None), + (generic_patch_1, resource_group, None), + (generic_patch_1, None, etag) + ] + ) + def test_update_component(self, fixture_cmd, service_client, json_patch, resource_group_name, etag): + result = subject.update_component( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + component_path=component_path, + json_patch=json_patch, + resource_group_name=resource_group_name, + etag=etag + ) + + start = check_resource_group_name_call(service_client, resource_group_name) + # check patch request + patch_request = service_client.calls[start].request + assert patch_request.method == "PATCH" + + expected_request_body = [json.loads(json_patch)] + assert json.loads(patch_request.body) == expected_request_body + + assert patch_request.headers["If-Match"] == etag if etag else "*" + + # check get request + get_request = service_client.calls[start + 1].request + assert get_request.method == "GET" + + assert result == json.loads(generic_result) + + @pytest.fixture(params=[400, 401, 500]) + def service_client_error(self, mocked_response, start_twin_response, request): + mocked_response.add( + method=responses.PATCH, + url="https://{}/digitaltwins/{}/components/{}".format( + hostname, twin_id, component_path + ), + body=generic_result, + status=request.param, + content_type="application/json", + match_querystring=False, + ) + + yield mocked_response + + def test_update_component_error(self, fixture_cmd, service_client_error): + with pytest.raises(CLIError): + subject.update_component( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + component_path=component_path, + json_patch=generic_patch_1, + resource_group_name=None, + etag=None + ) diff --git a/azext_iot/tests/dps/test_iot_dps_unit.py b/azext_iot/tests/dps/test_iot_dps_unit.py index 672fede11..91fde8687 100644 --- a/azext_iot/tests/dps/test_iot_dps_unit.py +++ b/azext_iot/tests/dps/test_iot_dps_unit.py @@ -454,6 +454,8 @@ def test_enrollment_update(self, serviceclient, req): assert "{}/enrollments/{}?".format(mock_target['entity'], enrollment_id) in url assert update_request.method == 'PUT' + assert update_request.headers["If-Match"] == req['etag'] if req['etag'] else "*" + body = json.loads(update_request.body) if not req['certificate_path']: if req['remove_certificate_path']: @@ -614,14 +616,19 @@ def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): ) yield mocked_response - def test_enrollment_delete(self, serviceclient): - subject.iot_dps_device_enrollment_delete(None, enrollment_id, - mock_target['entity'], resource_group) + @pytest.mark.parametrize( + "etag", + [None, etag] + ) + def test_enrollment_delete(self, serviceclient, etag): + subject.iot_dps_device_enrollment_delete(serviceclient, enrollment_id, + mock_target['entity'], resource_group, etag=etag) request = serviceclient.calls[0].request url = request.url method = request.method assert "{}/enrollments/{}?".format(mock_target['entity'], enrollment_id) in url assert method == 'DELETE' + assert request.headers["If-Match"] == etag if etag else "*" def test_enrollment_delete_error(self, serviceclient_generic_error): with pytest.raises(CLIError): @@ -1004,6 +1011,7 @@ def test_enrollment_group_update(self, serviceclient, req): assert "{}/enrollmentGroups/{}?".format(mock_target['entity'], enrollment_id) in url assert request.method == 'PUT' + assert request.headers["If-Match"] == req['etag'] if req['etag'] else "*" body = json.loads(request.body) if not req['certificate_path']: @@ -1211,14 +1219,19 @@ def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): ) yield mocked_response - def test_enrollment_group_delete(self, serviceclient): + @pytest.mark.parametrize( + "etag", + [None, etag] + ) + def test_enrollment_group_delete(self, serviceclient, etag): subject.iot_dps_device_enrollment_group_delete(None, enrollment_id, - mock_target['entity'], resource_group) + mock_target['entity'], resource_group, etag=etag) request = serviceclient.calls[0].request url = request.url method = request.method assert "{}/enrollmentGroups/{}?".format(mock_target['entity'], enrollment_id) in url assert method == 'DELETE' + assert request.headers["If-Match"] == etag if etag else "*" def test_enrollment_group_delete_error(self): with pytest.raises(CLIError): @@ -1302,14 +1315,19 @@ def serviceclient(self, mocked_response, fixture_gdcs, fixture_sas, request): ) yield mocked_response - def test_registration_delete(self, serviceclient): + @pytest.mark.parametrize( + "etag", + [None, etag] + ) + def test_registration_delete(self, serviceclient, etag): subject.iot_dps_registration_delete(None, mock_target['entity'], - resource_group, registration_id) + resource_group, registration_id, etag=etag) request = serviceclient.calls[0].request url = request.url method = request.method assert "{}/registrations/{}?".format(mock_target['entity'], registration_id) in url assert method == 'DELETE' + assert request.headers["If-Match"] == etag if etag else "*" def test_registration_delete_error(self): with pytest.raises(CLIError): From 38eb40c047cbd82eb26cd3a848d9a54b0df57e95 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Wed, 24 Feb 2021 14:56:09 -0800 Subject: [PATCH 173/179] Increment version and history. --- HISTORY.rst | 22 +++++++++++++++++++++- azext_iot/constants.py | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 79f82ed08..c010181de 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,26 @@ Release History =============== +0.10.10 ++++++++++++++++ + +**Azure Digital Twins updates** + +* Addition of the optional '--etag' argument for the following commands: + + * az dt twin [update | delete] + * az dt twin relationship [update | delete] + +* Addition of the optional '--if-not-match' switch for the following commands: + + * az dt twin create + * az dt twin relationship create + +**IoT Central updates** + +Placeholder + + 0.10.9 +++++++++++++++ @@ -19,7 +39,7 @@ Release History * Improve http debug logging. * Fix bug related to issue #296. Adds a clause to device-identity update that allows user to update primary-key / secondary-key -and primary-thumbprint / secondary-thumbprint values (respectively, per auth method) without needing to specify the auth_method in the update command. + and primary-thumbprint / secondary-thumbprint values (respectively, per auth method) without needing to specify the auth_method in the update command. 0.10.8 diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 8b8f419db..5e82e59da 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.9" +VERSION = "0.10.10" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" From 65028a59af10f60874f60dc648b03b5dc6df1432 Mon Sep 17 00:00:00 2001 From: vilit1 <73560279+vilit1@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:02:26 -0800 Subject: [PATCH 174/179] ADT refactor --replace to --if-none-match (#315) * Use if_none_match instead of replace * pylint error fix * update help messaages * update help and params, add etags to it --- azext_iot/digitaltwins/_help.py | 28 +++++ azext_iot/digitaltwins/commands_twins.py | 8 +- azext_iot/digitaltwins/params.py | 9 +- azext_iot/digitaltwins/providers/twin.py | 8 +- azext_iot/operations/hub.py | 2 +- .../test_dt_twin_lifecycle_int.py | 110 +++++++++++++++++- .../tests/digitaltwins/test_dt_twins_unit.py | 16 +-- 7 files changed, 157 insertions(+), 24 deletions(-) diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index a581f8d0c..8f73c472e 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -442,6 +442,11 @@ def load_digitaltwins_help(): az dt twin create -n {instance_or_hostname} --dtmi "dtmi:com:example:Room;1" --twin-id {twin_id} + - name: Create a digital twin from an existing (prior-created) model with if-none-match tag. + text: > + az dt twin create -n {instance_or_hostname} --dtmi "dtmi:com:example:Room;1" + --twin-id {twin_id} --if-none-match + - name: Create a digital twin from an existing (prior-created) model. Instantiate with property values. text: > az dt twin create -n {instance_or_hostname} --dtmi "dtmi:com:example:DeviceInformation;1" @@ -478,6 +483,11 @@ def load_digitaltwins_help(): az dt twin update -n {instance_or_hostname} --twin-id {twin_id} --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' + - name: Update a digital twin via JSON patch specification and using etag. + text: > + az dt twin update -n {instance_or_hostname} --twin-id {twin_id} --etag {etag} + --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' + - name: Update a digital twin via JSON patch specification. text: > az dt twin update -n {instance_or_hostname} --twin-id {twin_id} @@ -524,6 +534,10 @@ def load_digitaltwins_help(): - name: Remove a digital twin by Id. text: > az dt twin delete -n {instance_or_hostname} --twin-id {twin_id} + + - name: Remove a digital twin by Id using the etag. + text: > + az dt twin delete -n {instance_or_hostname} --twin-id {twin_id} --etag {etag} """ helps["dt twin relationship"] = """ @@ -542,6 +556,11 @@ def load_digitaltwins_help(): az dt twin relationship create -n {instance_or_hostname} --relationship-id {relationship_id} --relationship contains --twin-id {source_twin_id} --target {target_twin_id} + - name: Create a relationship between two digital twins with if-none-match tag + text: > + az dt twin relationship create -n {instance_or_hostname} --relationship-id {relationship_id} --relationship contains + --twin-id {source_twin_id} --target {target_twin_id} --if-none-match + - name: Create a relationship with initialized properties between two digital twins. text: > az dt twin relationship create -n {instance_or_hostname} --relationship-id {relationship_id} --relationship contains @@ -593,6 +612,11 @@ def load_digitaltwins_help(): az dt twin relationship update -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} --relationship contains --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' + - name: Update a digital twin relationship via JSON patch specification and using etag. + text: > + az dt twin relationship update -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} + --relationship contains --json-patch '{"op":"replace", "path":"/Temperature", "value": 20.5}' --etag {etag} + - name: Update a digital twin relationship via JSON patch specification. text: > az dt twin relationship update -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} @@ -615,6 +639,10 @@ def load_digitaltwins_help(): - name: Delete a digital twin relationship. text: > az dt twin relationship delete -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} + + - name: Delete a digital twin relationship using the etag. + text: > + az dt twin relationship delete -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} --etag {etag} """ helps["dt twin telemetry"] = """ diff --git a/azext_iot/digitaltwins/commands_twins.py b/azext_iot/digitaltwins/commands_twins.py index fb4570753..b219acd96 100644 --- a/azext_iot/digitaltwins/commands_twins.py +++ b/azext_iot/digitaltwins/commands_twins.py @@ -22,13 +22,13 @@ def create_twin( name_or_hostname, twin_id, model_id, - replace=False, + if_none_match=False, properties=None, resource_group_name=None ): twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) return twin_provider.create( - twin_id=twin_id, model_id=model_id, replace=replace, properties=properties + twin_id=twin_id, model_id=model_id, if_none_match=if_none_match, properties=properties ) @@ -54,7 +54,7 @@ def create_relationship( target_twin_id, relationship_id, relationship, - replace=False, + if_none_match=False, properties=None, resource_group_name=None, ): @@ -64,7 +64,7 @@ def create_relationship( target_twin_id=target_twin_id, relationship_id=relationship_id, relationship=relationship, - replace=replace, + if_none_match=if_none_match, properties=properties, ) diff --git a/azext_iot/digitaltwins/params.py b/azext_iot/digitaltwins/params.py index e54ca7c11..5214425b5 100644 --- a/azext_iot/digitaltwins/params.py +++ b/azext_iot/digitaltwins/params.py @@ -279,7 +279,8 @@ def load_digitaltwins_arguments(self, _): "Operations are limited to add, replace and remove. Provide file path or inline JSON.", ) context.argument( - "etag", options_list=["--etag", "-e"], help="Entity tag value." + "etag", options_list=["--etag", "-e"], help="Entity tag value. The command will succeed if " + "the etag matches the current etag for the resource." ) context.argument( "component_path", @@ -287,9 +288,9 @@ def load_digitaltwins_arguments(self, _): help="The path to the DTDL component.", ) context.argument( - "replace", - options_list=["--replace"], - help="Indicates the operation should replace an existing twin if it exists." + "if_none_match", + options_list=["--if-none-match"], + help="Indicates the create operation should fail if an existing twin with the same id exists." ) with self.argument_context("dt twin create") as context: diff --git a/azext_iot/digitaltwins/providers/twin.py b/azext_iot/digitaltwins/providers/twin.py index 5dada72d8..7bf8c06d1 100644 --- a/azext_iot/digitaltwins/providers/twin.py +++ b/azext_iot/digitaltwins/providers/twin.py @@ -57,7 +57,7 @@ def invoke_query(self, query, show_cost): return query_result - def create(self, twin_id, model_id, replace=False, properties=None): + def create(self, twin_id, model_id, if_none_match=False, properties=None): twin_request = { "$dtId": twin_id, "$metadata": {"$model": model_id}, @@ -72,7 +72,7 @@ def create(self, twin_id, model_id, replace=False, properties=None): logger.info("Twin payload %s", json.dumps(twin_request)) try: - options = TwinOptions(if_none_match=(None if replace else "*")) + options = TwinOptions(if_none_match=("*" if if_none_match else None)) return self.twins_sdk.add(id=twin_id, twin=twin_request, digital_twins_add_options=options) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) @@ -117,7 +117,7 @@ def add_relationship( target_twin_id, relationship_id, relationship, - replace=False, + if_none_match=False, properties=None, ): relationship_request = { @@ -133,7 +133,7 @@ def add_relationship( logger.info("Relationship payload %s", json.dumps(relationship_request)) try: - options = TwinOptions(if_none_match=(None if replace else "*")) + options = TwinOptions(if_none_match=("*" if if_none_match else None)) return self.twins_sdk.add_relationship( id=twin_id, relationship_id=relationship_id, diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index b85a8022f..22ec18854 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -2223,7 +2223,7 @@ def http_wrap(target, device_id, generator): max_runs=msg_count, return_handle=True, ) - while True and op.is_alive(): + while op.is_alive(): _handle_c2d_msg(target, device_id, receive_settle) sleep(SIM_RECEIVE_SLEEP_SEC) 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 defe13cf9..1c884fee4 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -30,6 +30,7 @@ def test_dt_twin(self): room_dtmi = "dtmi:com:example:Room;1" room_twin_id = "myroom" thermostat_component_id = "Thermostat" + etag = 'AAAA==' create_output = self.cmd( "dt create -n {} -g {} -l {}".format(instance_name, self.rg, self.region) @@ -112,7 +113,7 @@ def test_dt_twin(self): ) replaced_room_twin = self.cmd( - "dt twin create -n {} -g {} --dtmi {} --twin-id {} --replace --properties '{}'".format( + "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( instance_name, self.rg, room_dtmi, @@ -129,9 +130,9 @@ def test_dt_twin(self): component_name=thermostat_component_id, ) - # new twin cannot be created with same twin_id if replace not provided + # new twin cannot be created with same twin_id if if-none-match provided self.cmd( - "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( + "dt twin create -n {} -g {} --dtmi {} --twin-id {} --if-none-match --properties '{}'".format( instance_name, self.rg, room_dtmi, @@ -141,6 +142,17 @@ def test_dt_twin(self): expect_failure=True ) + # delete command should fail if etag is different + self.cmd( + "dt twin delete -n {} -g {} --twin-id {} --etag '{}'".format( + instance_name, + self.rg, + room_twin_id, + etag + ), + expect_failure=True + ) + self.cmd( "dt twin delete -n {} -g {} --twin-id {}".format( instance_name, @@ -241,6 +253,30 @@ def test_dt_twin(self): == json.loads(self.kwargs["temperatureJsonPatch"])["value"] ) + self.cmd( + "dt twin update -n {} --twin-id {} --json-patch '{}' --etag '{}'".format( + instance_name, + room_twin_id, + "{temperatureJsonPatch}", + etag + ), + expect_failure=True + ) + + update_twin_result = self.cmd( + "dt twin update -n {} --twin-id {} --json-patch '{}' --etag '{}'".format( + instance_name, + room_twin_id, + "{temperatureJsonPatch}", + update_twin_result["$etag"] + ) + ).get_output_in_json() + + assert ( + update_twin_result["Temperature"] + == json.loads(self.kwargs["temperatureJsonPatch"])["value"] + ) + twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins'".format( instance_name, self.rg @@ -257,6 +293,18 @@ def test_dt_twin(self): {"op": "replace", "path": "/ownershipUser", "value": "meme"} ) + twin_relationship_create_result = self.cmd( + "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --twin-id {} " + "--target-twin-id {}".format( + instance_name, + self.rg, + relationship_id, + relationship, + floor_twin_id, + room_twin_id, + ) + ).get_output_in_json() + twin_relationship_create_result = self.cmd( "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --twin-id {} " "--target-twin-id {} --properties '{}'".format( @@ -279,6 +327,21 @@ def test_dt_twin(self): properties=self.kwargs["relationshipJson"], ) + # new twin cannot be created with same twin_id if if-none-match provided + twin_relationship_create_result = self.cmd( + "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --twin-id {} " + "--target-twin-id {} --if-none-match --properties '{}'".format( + instance_name, + self.rg, + relationship_id, + relationship, + floor_twin_id, + room_twin_id, + "{relationshipJson}", + ), + expect_failure=True + ) + twin_relationship_show_result = self.cmd( "dt twin relationship show -n {} -g {} --twin-id {} --relationship-id {}".format( instance_name, @@ -313,6 +376,37 @@ def test_dt_twin(self): == json.loads(self.kwargs["relationshipJsonPatch"])["value"] ) + # Fail to update if the etag if different + self.cmd( + "dt twin relationship update -n {} -g {} --relationship-id {} --twin-id {} " + "--json-patch '{}' --etag '{}'".format( + instance_name, + self.rg, + relationship_id, + floor_twin_id, + "{relationshipJsonPatch}", + etag + ), + expect_failure=True + ) + + twin_edge_update_result = self.cmd( + "dt twin relationship update -n {} -g {} --relationship-id {} --twin-id {} " + "--json-patch '{}' --etag '{}'".format( + instance_name, + self.rg, + relationship_id, + floor_twin_id, + "{relationshipJsonPatch}", + twin_edge_update_result["$etag"] + ) + ).get_output_in_json() + + assert ( + twin_edge_update_result["ownershipUser"] + == json.loads(self.kwargs["relationshipJsonPatch"])["value"] + ) + twin_relationship_list_result = self.cmd( "dt twin relationship list -n {} --twin-id {}".format( instance_name, @@ -354,6 +448,16 @@ def test_dt_twin(self): ).get_output_in_json() assert len(twin_relationship_list_result) == 1 + self.cmd( + "dt twin relationship delete -n {} --twin-id {} -r {} --etag '{}'".format( + instance_name, + floor_twin_id, + relationship_id, + etag + ), + expect_failure=True + ) + # No output from API for delete edge self.cmd( "dt twin relationship delete -n {} --twin-id {} -r {}".format( diff --git a/azext_iot/tests/digitaltwins/test_dt_twins_unit.py b/azext_iot/tests/digitaltwins/test_dt_twins_unit.py index e337f3b67..eb6141e97 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twins_unit.py +++ b/azext_iot/tests/digitaltwins/test_dt_twins_unit.py @@ -247,7 +247,7 @@ def service_client(self, mocked_response, start_twin_response): yield mocked_response @pytest.mark.parametrize( - "replace, properties, resource_group_name", + "if_none_match, properties, resource_group_name", [ (False, None, None), (True, None, None), @@ -256,13 +256,13 @@ def service_client(self, mocked_response, start_twin_response): (False, generic_patch_2, None) ] ) - def test_create_twin(self, fixture_cmd, service_client, replace, properties, resource_group_name): + def test_create_twin(self, fixture_cmd, service_client, if_none_match, properties, resource_group_name): result = subject.create_twin( cmd=fixture_cmd, name_or_hostname=hostname, twin_id=twin_id, model_id=model_id, - replace=replace, + if_none_match=if_none_match, properties=properties, resource_group_name=resource_group_name ) @@ -277,7 +277,7 @@ def test_create_twin(self, fixture_cmd, service_client, replace, properties, res assert twin_request_body["$dtId"] == twin_id assert twin_request_body["$metadata"]["$model"] == model_id - if not replace: + if if_none_match: assert twin_request.headers["If-None-Match"] == "*" if properties: @@ -557,7 +557,7 @@ def service_client(self, mocked_response, start_twin_response): yield mocked_response @pytest.mark.parametrize( - "relationship, replace, properties, resource_group_name", + "relationship, if_none_match, properties, resource_group_name", [ ("contains", False, None, None), ("", False, None, None), @@ -567,7 +567,7 @@ def service_client(self, mocked_response, start_twin_response): ("contains", False, None, resource_group) ] ) - def test_create_relationship(self, fixture_cmd, service_client, relationship, replace, properties, resource_group_name): + def test_create_relationship(self, fixture_cmd, service_client, relationship, if_none_match, properties, resource_group_name): result = subject.create_relationship( cmd=fixture_cmd, name_or_hostname=hostname, @@ -575,7 +575,7 @@ def test_create_relationship(self, fixture_cmd, service_client, relationship, re target_twin_id=target_twin_id, relationship_id=relationship_id, relationship=relationship, - replace=replace, + if_none_match=if_none_match, properties=properties, resource_group_name=resource_group_name ) @@ -591,7 +591,7 @@ def test_create_relationship(self, fixture_cmd, service_client, relationship, re assert result_request_body["$targetId"] == target_twin_id assert result_request_body["$relationshipName"] == relationship - if not replace: + if if_none_match: assert put_request.headers["If-None-Match"] == "*" if properties: From d830e62fdcbf15100d871a5888184e7452cfdf8f Mon Sep 17 00:00:00 2001 From: vilit1 <73560279+vilit1@users.noreply.github.com> Date: Tue, 16 Mar 2021 15:42:14 -0700 Subject: [PATCH 175/179] Add error handling for hub connection-string show (#320) * Add error handling for hub connection-string show. * Update help message. * Updates based on PR feedback. * Default in params does not work as expected. --- azext_iot/_help.py | 14 +++++------ azext_iot/_params.py | 8 +++--- azext_iot/operations/hub.py | 29 +++++++++++++++------- azext_iot/tests/iothub/test_iot_ext_int.py | 26 +++++++++++++++---- 4 files changed, 52 insertions(+), 25 deletions(-) diff --git a/azext_iot/_help.py b/azext_iot/_help.py index 7be342f37..8667e4cda 100644 --- a/azext_iot/_help.py +++ b/azext_iot/_help.py @@ -107,24 +107,24 @@ "iot hub connection-string show" ] = """ type: command - short-summary: Show the connection strings for an IoT Hub. + short-summary: Show the connection strings for the specified IoT Hubs using the given policy name and key. examples: - - name: Show the connection string for all IoT Hubs in a subscription using the default policy and primary key. + - name: Show the connection strings for all active state IoT Hubs in a subscription using the default policy and primary key. text: > az iot hub connection-string show - - name: Show the connection string for all IoT Hubs in a resource group using the default policy and primary key. + - name: Show the connection strings for all active state IoT Hubs in a resource group using the default policy and primary key. text: > az iot hub connection-string show --resource-group MyResourceGroup - - name: Show all the connection string of an IoT Hub using primary key. + - name: Show all connection strings of the given IoT Hub using primary key. text: > az iot hub connection-string show -n MyIotHub --all - - name: Show the connection string of an IoT Hub using default policy and primary key. + - name: Show the connection string of the given IoT Hub using the default policy and primary key. text: > az iot hub connection-string show -n MyIotHub - - name: Show the connection string of an IoT Hub using policy 'service' and secondary key. + - name: Show the connection string of the given IoT Hub using policy 'service' and secondary key. text: > az iot hub connection-string show -n MyIotHub --policy-name service --key-type secondary - - name: Show the eventhub compatible connection string of an IoT Hub\'s default eventhub. + - name: Show the eventhub compatible connection string of the given IoT Hub\'s default eventhub. text: > az iot hub connection-string show -n MyIotHub --default-eventhub """ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 8d0520fb4..91cfb4314 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -100,12 +100,12 @@ def load_arguments(self, _): "key_type", options_list=["--key-type", "--kt"], arg_type=get_enum_type(KeyType), - help="Shared access policy key type for auth.", + help="Shared access policy key type for authentication.", ) context.argument( "policy_name", options_list=["--policy-name", "--pn"], - help="Shared access policy to use for auth.", + help="Shared access policy to use for authentication.", ) context.argument( "duration", @@ -266,13 +266,13 @@ def load_arguments(self, _): context.argument( "show_all", options_list=["--show-all", "--all"], - help="Allow to show all shared access policies.", + help="Show all shared access policies for the respective IoT Hub.", ) context.argument( "default_eventhub", arg_type=get_three_state_flag(), options_list=["--default-eventhub", "--eh"], - help="Flag indicating the connection string returned is for the default EventHub endpoint. Default: false", + help="Flag indicating the connection string returned is for the default EventHub endpoint. Default: false.", ) with self.argument_context("iot hub job") as context: diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 22ec18854..6b270b94c 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -2651,15 +2651,26 @@ def conn_str_getter(hub): discovery, hub, policy_name, key_type, show_all, default_eventhub ) - return [ - { - "name": hub.name, - "connectionString": conn_str_getter(hub) - if show_all - else conn_str_getter(hub)[0], - } - for hub in hubs if hub.properties.state == IoTHubStateType.Active.value - ] + connection_strings = [] + 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], + }) + 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}.") + 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.") + return connection_strings + hub = discovery.find_iothub(hub_name, resource_group_name) if hub: conn_str = _get_hub_connection_string( diff --git a/azext_iot/tests/iothub/test_iot_ext_int.py b/azext_iot/tests/iothub/test_iot_ext_int.py index 0114e3c37..81b67f0f5 100644 --- a/azext_iot/tests/iothub/test_iot_ext_int.py +++ b/azext_iot/tests/iothub/test_iot_ext_int.py @@ -74,17 +74,33 @@ def test_hub(self): # 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://' + conn_str_eventhub_pattern = (r'^Endpoint=sb://(.+?)servicebus.windows.net/;SharedAccessKeyName=' + r'iothubowner;SharedAccessKey=(.+?);EntityPath=') + defaultpolicy = "iothubowner" + nonexistantpolicy = "badpolicy" - # TODO: Temporarily disable to support warning on missing policy. - # 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) + 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) ]) From f3767964ad86cadf21801c30fa986119f1f128cb Mon Sep 17 00:00:00 2001 From: Ryan K Date: Fri, 16 Apr 2021 16:21:38 -0700 Subject: [PATCH 176/179] Update jsonschema dependency and remove pkg_resources working set workaround (#328) --- azext_iot/common/utility.py | 13 ------------- azext_iot/models/__init__.py | 5 ----- setup.py | 2 +- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index ff2f2a128..bcb27e641 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -412,19 +412,6 @@ def looks_like_file(element): ) -def ensure_pkg_resources_entries(): - import pkg_resources - - from azure.cli.core.extension import get_extension_path - from azext_iot.constants import EXTENSION_NAME - - extension_path = get_extension_path(EXTENSION_NAME) - if extension_path not in pkg_resources.working_set.entries: - pkg_resources.working_set.add_entry(extension_path) - - return - - class ISO8601Validator: def is_iso8601_date(self, to_validate) -> bool: try: diff --git a/azext_iot/models/__init__.py b/azext_iot/models/__init__.py index 6b79613f9..55614acbf 100644 --- a/azext_iot/models/__init__.py +++ b/azext_iot/models/__init__.py @@ -3,8 +3,3 @@ # 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.utility import ensure_pkg_resources_entries - -ensure_pkg_resources_entries() diff --git a/setup.py b/setup.py index 501122e41..f5397799c 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ # though that is installed out of band (managed by the extension) # for compatibility reasons. -DEPENDENCIES = ["paho-mqtt==1.5.0", "jsonschema==3.0.2", "setuptools"] +DEPENDENCIES = ["paho-mqtt==1.5.0", "jsonschema==3.2.0", "setuptools"] CLASSIFIERS = [ From d95cf8c13dcf9931f1a20542cbf1db28eb80cd0d Mon Sep 17 00:00:00 2001 From: vilit1 <73560279+vilit1@users.noreply.github.com> Date: Wed, 21 Apr 2021 16:20:09 -0700 Subject: [PATCH 177/179] Add twin delete all and twin relationship delete all (#331) --- azext_iot/digitaltwins/_help.py | 24 ++ azext_iot/digitaltwins/command_map.py | 2 + azext_iot/digitaltwins/commands_twins.py | 17 +- azext_iot/digitaltwins/providers/twin.py | 46 ++- .../test_dt_twin_lifecycle_int.py | 182 ++++++++++- .../tests/digitaltwins/test_dt_twins_unit.py | 307 +++++++++++++++++- 6 files changed, 558 insertions(+), 20 deletions(-) diff --git a/azext_iot/digitaltwins/_help.py b/azext_iot/digitaltwins/_help.py index 8f73c472e..49afe67a2 100644 --- a/azext_iot/digitaltwins/_help.py +++ b/azext_iot/digitaltwins/_help.py @@ -540,6 +540,16 @@ def load_digitaltwins_help(): az dt twin delete -n {instance_or_hostname} --twin-id {twin_id} --etag {etag} """ + helps["dt twin delete-all"] = """ + type: command + short-summary: Deletes all digital twins within a Digital Twins instance, including all relationships for those twins. + + examples: + - name: Delete all digital twins. Any relationships referencing the twins will also be deleted. + text: > + az dt twin delete-all -n {instance_or_hostname} + """ + helps["dt twin relationship"] = """ type: group short-summary: Manage and configure the digital twin relationships of a Digital Twins instance. @@ -645,6 +655,20 @@ def load_digitaltwins_help(): az dt twin relationship delete -n {instance_or_hostname} --twin-id {twin_id} --relationship-id {relationship_id} --etag {etag} """ + helps["dt twin relationship delete-all"] = """ + type: command + short-summary: Deletes all digital twin relationships within a Digital Twins instance, including incoming relationships. + + examples: + - name: Delete all digital twin relationships associated with the twin. + text: > + az dt twin relationship delete-all -n {instance_or_hostname} --twin-id {twin_id} + + - name: Delete all digital twin relationships within the Digital Twins instace. + text: > + az dt twin relationship delete-all -n {instance_or_hostname} + """ + helps["dt twin telemetry"] = """ type: group short-summary: Test and validate the event routes and endpoints of a Digital Twins instance. diff --git a/azext_iot/digitaltwins/command_map.py b/azext_iot/digitaltwins/command_map.py index 3b7df0b27..22924cb4b 100644 --- a/azext_iot/digitaltwins/command_map.py +++ b/azext_iot/digitaltwins/command_map.py @@ -100,6 +100,7 @@ def load_digitaltwins_commands(self, _): cmd_group.show_command("show", "show_twin") cmd_group.command("update", "update_twin") cmd_group.command("delete", "delete_twin") + cmd_group.command("delete-all", "delete_all_twin", confirmation=True) with self.command_group( "dt twin component", command_type=digitaltwins_twin_ops @@ -115,6 +116,7 @@ def load_digitaltwins_commands(self, _): cmd_group.command("list", "list_relationships") cmd_group.command("update", "update_relationship") cmd_group.command("delete", "delete_relationship") + cmd_group.command("delete-all", "delete_all_relationship", confirmation=True) with self.command_group( "dt twin telemetry", command_type=digitaltwins_twin_ops diff --git a/azext_iot/digitaltwins/commands_twins.py b/azext_iot/digitaltwins/commands_twins.py index b219acd96..8eea79b4d 100644 --- a/azext_iot/digitaltwins/commands_twins.py +++ b/azext_iot/digitaltwins/commands_twins.py @@ -44,7 +44,12 @@ def update_twin(cmd, name_or_hostname, twin_id, json_patch, resource_group_name= def delete_twin(cmd, name_or_hostname, twin_id, resource_group_name=None, etag=None): twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) - return twin_provider.delete(twin_id, etag=etag) + return twin_provider.delete(twin_id=twin_id, etag=etag) + + +def delete_all_twin(cmd, name_or_hostname, resource_group_name=None): + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) + return twin_provider.delete_all() def create_relationship( @@ -118,6 +123,16 @@ def delete_relationship( ) +def delete_all_relationship( + cmd, name_or_hostname, twin_id=None, resource_group_name=None +): + twin_provider = TwinProvider(cmd=cmd, name=name_or_hostname, rg=resource_group_name) + if twin_id: + return twin_provider.delete_all_relationship(twin_id=twin_id) + else: + return twin_provider.delete_all(only_relationships=True) + + def send_telemetry( cmd, name_or_hostname, diff --git a/azext_iot/digitaltwins/providers/twin.py b/azext_iot/digitaltwins/providers/twin.py index 7bf8c06d1..e07bf4c92 100644 --- a/azext_iot/digitaltwins/providers/twin.py +++ b/azext_iot/digitaltwins/providers/twin.py @@ -104,13 +104,29 @@ def update(self, twin_id, json_patch, etag=None): raise CLIError(unpack_msrest_error(e)) def delete(self, twin_id, etag=None): - # Not a json response try: options = TwinOptions(if_match=(etag if etag else "*")) self.twins_sdk.delete(id=twin_id, digital_twins_delete_options=options, raw=True) except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) + def delete_all(self, only_relationships=False): + # need to get all twins + query = "select * from digitaltwins" + twins = self.invoke_query(query=query, show_cost=False)["result"] + print(f"Found {len(twins)} twin(s).") + + # go through and delete all + for twin in twins: + try: + self.delete_all_relationship( + twin_id=twin["$dtId"] + ) + if not only_relationships: + self.delete(twin_id=twin["$dtId"]) + except CLIError as e: + logger.warn(f"Could not delete twin {twin['$dtId']}. The error is {e}") + def add_relationship( self, twin_id, @@ -214,6 +230,34 @@ def delete_relationship(self, twin_id, relationship_id, etag=None): except ErrorResponseException as e: raise CLIError(unpack_msrest_error(e)) + def delete_all_relationship(self, twin_id): + relationships = self.list_relationships(twin_id, incoming_relationships=True) + incoming_pager = self.list_relationships(twin_id) + + # relationships pager needs to be advanced to get relationships + try: + while True: + relationships.extend(incoming_pager.advance_page()) + except StopIteration: + pass + + print(f"Found {len(relationships)} relationship(s) associated with twin {twin_id}.") + + for relationship in relationships: + try: + if isinstance(relationship, dict): + self.delete_relationship( + twin_id=twin_id, + relationship_id=relationship['$relationshipId'] + ) + else: + self.delete_relationship( + twin_id=relationship.source_id, + relationship_id=relationship.relationship_id + ) + except CLIError as e: + logger.warn(f"Could not delete relationship {relationship}. The error is {e}.") + def get_component(self, twin_id, component_path): try: return self.twins_sdk.get_component( 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 1c884fee4..7f6f77d35 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -284,6 +284,7 @@ def test_dt_twin(self): ).get_output_in_json() assert len(twin_query_result["result"]) == 2 + # Relationship Tests relationship_id = "myedge" relationship = "contains" self.kwargs["relationshipJson"] = json.dumps( @@ -500,16 +501,181 @@ def test_dt_twin(self): ) ) - for twin_tuple in twins_id_list: - # No output from API for delete twin - self.cmd( - "dt twin delete -n {} --twin-id {} {}".format( - instance_name, - twin_tuple[0], - "-g {}".format(self.rg) if twins_id_list[-1] == twin_tuple else "", - ) + self.cmd( + "dt twin delete-all -n {} --yes".format( + instance_name, ) + ) sleep(10) # Wait for API to catch up + + 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 + assert twin_query_result["cost"] + + def test_dt_twin_delete(self): + self.wait_for_capacity() + instance_name = generate_resource_id() + models_directory = "./models" + floor_dtmi = "dtmi:com:example:Floor;1" + floor_twin_id = "myfloor" + room_dtmi = "dtmi:com:example:Room;1" + room_twin_id = "myroom" + thermostat_component_id = "Thermostat" + + create_output = self.cmd( + "dt create -n {} -g {} -l {}".format(instance_name, self.rg, self.region) + ).get_output_in_json() + self.track_instance(create_output) + self.wait_for_hostname(create_output) + + self.cmd( + "dt role-assignment create -n {} -g {} --assignee {} --role '{}'".format( + instance_name, self.rg, self.current_user, self.role_map["owner"] + ) + ) + # Wait for RBAC to catch-up + sleep(60) + + self.cmd( + "dt model create -n {} --from-directory '{}'".format( + instance_name, models_directory + ) + ) + + self.kwargs["tempAndThermostatComponentJson"] = json.dumps( + { + "Temperature": 10.2, + "Thermostat": { + "$metadata": {}, + "setPointTemp": 23.12, + }, + } + ) + + floor_twin = self.cmd( + "dt twin create -n {} --dtmi {} --twin-id {}".format( + instance_name, floor_dtmi, floor_twin_id + ) + ).get_output_in_json() + + assert_twin_attributes( + twin=floor_twin, + expected_twin_id=floor_twin_id, + expected_dtmi=floor_dtmi, + ) + + room_twin = self.cmd( + "dt twin create -n {} -g {} --dtmi {} --twin-id {} --properties '{}'".format( + instance_name, + self.rg, + room_dtmi, + room_twin_id, + "{tempAndThermostatComponentJson}", + ) + ).get_output_in_json() + + assert_twin_attributes( + twin=room_twin, + expected_twin_id=room_twin_id, + expected_dtmi=room_dtmi, + properties=self.kwargs["tempAndThermostatComponentJson"], + component_name=thermostat_component_id, + ) + + twin_query_result = self.cmd( + "dt twin query -n {} -g {} -q 'select * from digitaltwins'".format( + instance_name, self.rg + ) + ).get_output_in_json() + assert len(twin_query_result["result"]) == 2 + + # Relationship Tests + relationship_id = "myedge" + relationship = "contains" + self.kwargs["relationshipJson"] = json.dumps( + {"ownershipUser": "me", "ownershipDepartment": "mydepartment"} + ) + + twin_relationship_create_result = self.cmd( + "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --twin-id {} " + "--target-twin-id {} --properties '{}'".format( + instance_name, + self.rg, + relationship_id, + relationship, + floor_twin_id, + room_twin_id, + "{relationshipJson}", + ) + ).get_output_in_json() + + assert_twin_relationship_attributes( + twin_relationship_obj=twin_relationship_create_result, + expected_relationship=relationship, + relationship_id=relationship_id, + source_id=floor_twin_id, + target_id=room_twin_id, + properties=self.kwargs["relationshipJson"], + ) + + twin_relationship_list_result = self.cmd( + "dt twin relationship list -n {} --twin-id {}".format( + instance_name, + floor_twin_id, + ) + ).get_output_in_json() + assert len(twin_relationship_list_result) == 1 + + # Delete all relationships + self.cmd( + "dt twin relationship delete-all -n {} --twin-id {} --yes".format( + instance_name, + floor_twin_id, + ) + ) + + twin_relationship_list_result = self.cmd( + "dt twin relationship list -n {} --twin-id {}".format( + instance_name, + floor_twin_id, + ) + ).get_output_in_json() + assert len(twin_relationship_list_result) == 0 + + # Recreate relationship for delete all twins + twin_relationship_create_result = self.cmd( + "dt twin relationship create -n {} -g {} --relationship-id {} --relationship {} --twin-id {} " + "--target-twin-id {} --properties '{}'".format( + instance_name, + self.rg, + relationship_id, + relationship, + floor_twin_id, + room_twin_id, + "{relationshipJson}", + ) + ).get_output_in_json() + + assert_twin_relationship_attributes( + twin_relationship_obj=twin_relationship_create_result, + expected_relationship=relationship, + relationship_id=relationship_id, + source_id=floor_twin_id, + target_id=room_twin_id, + properties=self.kwargs["relationshipJson"], + ) + + self.cmd( + "dt twin delete-all -n {} --yes".format( + instance_name, + ) + ) + sleep(10) # Wait for API to catch up + twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins' --cost".format( instance_name, self.rg diff --git a/azext_iot/tests/digitaltwins/test_dt_twins_unit.py b/azext_iot/tests/digitaltwins/test_dt_twins_unit.py index eb6141e97..f5237f70e 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twins_unit.py +++ b/azext_iot/tests/digitaltwins/test_dt_twins_unit.py @@ -31,20 +31,21 @@ generic_patch_2 = json.dumps({"a" : "b", "c" : "d"}) -def generate_twin_result(twin_id=twin_id, etag=etag, model_id=model_id): - return json.dumps({ - "$dtId": twin_id, - "$etag": etag, +def generate_twin_result(randomized=False): + return { + "$dtId": generate_generic_id() if randomized else twin_id, + "$etag": generate_generic_id() if randomized else etag, "$metadata": { - "$model": model_id + "$model": generate_generic_id() if randomized else model_id } - }) + } def create_relationship(relationship_name=None): return { "$relationshipId": generate_generic_id(), - "relationship_name": relationship_name + "$relationshipName": relationship_name, + "$sourceId": generate_generic_id() } @@ -390,7 +391,7 @@ def service_client(self, mocked_response, start_twin_response): url="https://{}/digitaltwins/{}".format( hostname, twin_id ), - body=generate_twin_result(), + body=json.dumps(generate_twin_result()), status=200, content_type="application/json", match_querystring=False, @@ -433,7 +434,7 @@ def test_update_twin(self, fixture_cmd, service_client, json_patch, resource_gro get_request = service_client.calls[start + 1].request assert get_request.method == "GET" - assert result == json.loads(generate_twin_result()) + assert result == generate_twin_result() @pytest.fixture(params=[(400, 200), (401, 200), (500, 200), (202, 400), (202, 401), (202, 500)]) def service_client_error(self, mocked_response, start_twin_response, request): @@ -540,6 +541,105 @@ def test_delete_twin_error(self, fixture_cmd, service_client_error, resource_gro ) +class TestTwinDeleteAllTwin(object): + @pytest.fixture + def service_client_all(self, mocked_response, start_twin_response): + yield mocked_response + + @pytest.mark.parametrize( + "number_twins", [0, 1, 3] + ) + def test_delete_twin_all(self, mocker, fixture_cmd, service_client_all, number_twins): + + # Create query call and delete calls + query_result = [] + for i in range(number_twins): + twin = generate_twin_result(randomized=True) + query_result.append(twin) + # Query calls to check if there are any relationships + service_client_all.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/incomingrelationships".format( + hostname, twin["$dtId"] + ), + body=json.dumps({ + "value" : [], + "nextLink" : None + }), + status=200, + content_type="application/json", + match_querystring=False + ) + service_client_all.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/relationships".format( + hostname, twin["$dtId"] + ), + body=json.dumps({ + "value" : [], + "nextLink" : None + }), + status=200, + content_type="application/json", + match_querystring=False + ) + # Delete call + service_client_all.add( + method=responses.DELETE, + url="https://{}/digitaltwins/{}".format( + hostname, twin["$dtId"] + ), + body=generic_result, + status=204 if i % 2 == 0 else 400, + content_type="application/json", + match_querystring=False, + ) + # Query call for twins to delete + service_client_all.add( + method=responses.POST, + url="https://{}/query".format( + hostname + ), + body=json.dumps({ + "value": query_result, + "continuationToken": None + }), + status=200, + content_type="application/json", + match_querystring=False, + headers={ + "Query-Charge": "1.0" + } + ) + + # Call the delete all command + result = subject.delete_all_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + ) + + start = check_resource_group_name_call(service_client_all, resource_group_input=None) + + delete_request = service_client_all.calls[start].request + assert delete_request.method == "POST" + + # Check delete calls + for i in range(number_twins): + query1_request = service_client_all.calls[start + 1 + 3 * i].request + assert query1_request.method == "GET" + assert query_result[i]["$dtId"] in query1_request.url + + query2_request = service_client_all.calls[start + 2 + 3 * i].request + assert query2_request.method == "GET" + assert query_result[i]["$dtId"] in query2_request.url + + delete_request = service_client_all.calls[start + 3 + 3 * i].request + assert delete_request.method == "DELETE" + assert query_result[i]["$dtId"] in delete_request.url + + assert result is None + + class TestTwinCreateRelationship(object): @pytest.fixture def service_client(self, mocked_response, start_twin_response): @@ -870,7 +970,7 @@ def test_list_relationship( expected_result = [ x for x in servresult - if x["relationship_name"] and x["relationship_name"] == relationship + if x["$relationshipName"] and x["$relationshipName"] == relationship ] assert result == expected_result else: @@ -990,6 +1090,193 @@ def test_delete_relationship_error(self, fixture_cmd, service_client_error): ) +class TestTwinDeleteAllRelationship(object): + @pytest.fixture + def service_client_all(self, mocked_response, start_twin_response): + yield mocked_response + + @pytest.mark.parametrize( + "incoming, outcoming", + [ + (0, 0), + (1, 0), + (0, 1), + (1, 1), + (3, 0), + (0, 3), + (3, 3), + ] + ) + def test_delete_relationship_all(self, mocker, fixture_cmd, service_client_all, incoming, outcoming): + # Create query call with incoming_relationships=True + incoming_query = [] + for i in range(incoming): + relationship = create_relationship("contains") + incoming_query.append(relationship) + service_client_all.add( + method=responses.DELETE, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, relationship["$sourceId"], relationship["$relationshipId"] + ), + body=generic_result, + status=204 if i % 2 == 0 else 400, + content_type="application/json", + match_querystring=False, + ) + service_client_all.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/incomingrelationships".format( + hostname, twin_id + ), + body=json.dumps({ + "value" : incoming_query, + "nextLink" : None + }), + status=200, + content_type="application/json", + match_querystring=False + ) + + # Create query call with incoming_relationships=False + outcoming_query = [] + for i in range(incoming): + relationship = create_relationship("contains") + outcoming_query.append(relationship) + service_client_all.add( + method=responses.DELETE, + url="https://{}/digitaltwins/{}/relationships/{}".format( + hostname, twin_id, relationship["$relationshipId"] + ), + body=generic_result, + status=204 if i % 2 == 0 else 400, + content_type="application/json", + match_querystring=False, + ) + service_client_all.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/relationships".format( + hostname, twin_id + ), + body=json.dumps({ + "value" : outcoming_query, + "nextLink" : None + }), + status=200, + content_type="application/json", + match_querystring=False + ) + + # Run the delete all command + result = subject.delete_all_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + ) + + start = check_resource_group_name_call(service_client_all, resource_group_input=None) + + # First two calls should be the query calls + assert service_client_all.calls[start].request.method == "GET" + assert service_client_all.calls[start + 1].request.method == "GET" + + call_num = start + 2 + for i in range(len(incoming_query)): + delete_request = service_client_all.calls[call_num + i].request + assert delete_request.method == "DELETE" + assert incoming_query[i]["$relationshipId"] in delete_request.url + + call_num += len(incoming_query) + for i in range(len(outcoming_query)): + delete_request = service_client_all.calls[call_num + i].request + assert delete_request.method == "DELETE" + assert outcoming_query[i]["$relationshipId"] in delete_request.url + assert len(service_client_all.calls) == call_num + len(outcoming_query) + + assert result is None + + @pytest.fixture + def service_client(self, mocked_response, start_twin_response): + yield mocked_response + + @pytest.mark.parametrize( + "number_twins", [0, 1, 3] + ) + def test_delete_relationships_all_twins(self, mocker, fixture_cmd, service_client, number_twins): + # Create query call and delete calls + query_result = [] + for i in range(number_twins): + twin = generate_twin_result(randomized=True) + query_result.append(twin) + # Query calls to check if there are any relationships + service_client.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/incomingrelationships".format( + hostname, twin["$dtId"] + ), + body=json.dumps({ + "value" : [], + "nextLink" : None + }), + status=200, + content_type="application/json", + match_querystring=False + ) + service_client.add( + method=responses.GET, + url="https://{}/digitaltwins/{}/relationships".format( + hostname, twin["$dtId"] + ), + body=json.dumps({ + "value" : [], + "nextLink" : None + }), + status=200, + content_type="application/json", + match_querystring=False + ) + # the only difference between this and delete_all_twins is no twin delete call + # Query call for twins to delete + service_client.add( + method=responses.POST, + url="https://{}/query".format( + hostname + ), + body=json.dumps({ + "value": query_result, + "continuationToken": None + }), + status=200, + content_type="application/json", + match_querystring=False, + headers={ + "Query-Charge": "1.0" + } + ) + + # Call the delete all command + result = subject.delete_all_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + ) + + start = check_resource_group_name_call(service_client, resource_group_input=None) + + delete_request = service_client.calls[start].request + assert delete_request.method == "POST" + + # Check delete calls + for i in range(number_twins): + query1_request = service_client.calls[start + 1 + 2 * i].request + assert query1_request.method == "GET" + assert query_result[i]["$dtId"] in query1_request.url + + query2_request = service_client.calls[start + 2 + 2 * i].request + assert query2_request.method == "GET" + assert query_result[i]["$dtId"] in query2_request.url + + assert result is None + + class TestTwinSendTelemetry(object): @pytest.fixture def service_client(self, mocked_response, start_twin_response): From 0c9f0f480cea5148b5fe424b6985e068b6901091 Mon Sep 17 00:00:00 2001 From: Ryan K Date: Thu, 22 Apr 2021 16:32:29 -0700 Subject: [PATCH 178/179] Added support for track 2 SDKs (azure-mgmt-iothub >= 1.0.0) (#329) --- azext_iot/common/utility.py | 10 ++-- azext_iot/constants.py | 3 ++ azext_iot/iothub/providers/base.py | 2 +- azext_iot/iothub/providers/discovery.py | 51 +++++++++++-------- azext_iot/operations/hub.py | 13 +++-- .../tests/iothub/jobs/test_iothub_jobs_int.py | 15 ++++++ .../tests/iothub/test_iothub_discovery_int.py | 6 +-- .../tests/utility/test_iot_utility_unit.py | 11 ++-- setup.py | 4 +- 9 files changed, 75 insertions(+), 40 deletions(-) diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index bcb27e641..e116f1d6d 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -438,10 +438,14 @@ def is_iso8601_time(self, to_validate: str) -> bool: return False -def ensure_min_version(cur_ver, min_ver): - from pkg_resources._vendor.packaging import version +def ensure_iothub_sdk_min_version(min_ver): + from packaging import version + try: + from azure.mgmt.iothub import __version__ as iot_sdk_version + except ImportError: + from azure.mgmt.iothub._configuration import VERSION as iot_sdk_version - return version.parse(cur_ver) >= version.parse(min_ver) + return version.parse(iot_sdk_version) >= version.parse(min_ver) def scantree(path): diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 5e82e59da..7ed91cbc5 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -48,3 +48,6 @@ # Config Key's CONFIG_KEY_UAMQP_EXT_VERSION = "uamqp_ext_version" + +# Initial Track 2 SDK version +IOTHUB_TRACK_2_SDK_MIN_VERSION = '1.0.0' diff --git a/azext_iot/iothub/providers/base.py b/azext_iot/iothub/providers/base.py index cac0d7e5b..e668264dc 100644 --- a/azext_iot/iothub/providers/base.py +++ b/azext_iot/iothub/providers/base.py @@ -21,7 +21,7 @@ def __init__(self, cmd, hub_name, rg, login=None): self.discovery = IotHubDiscovery(cmd) self.target = self.discovery.get_target( hub_name=self.hub_name, - rg=self.rg, + resource_group_name=self.rg, login=login, ) self.resolver = SdkResolver(self.target) diff --git a/azext_iot/iothub/providers/discovery.py b/azext_iot/iothub/providers/discovery.py index 34069401f..4d9043d4a 100644 --- a/azext_iot/iothub/providers/discovery.py +++ b/azext_iot/iothub/providers/discovery.py @@ -6,10 +6,13 @@ from knack.util import CLIError from knack.log import get_logger -from azext_iot.common.utility import trim_from_start +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.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 enum import Enum, EnumMeta PRIVILEDGED_ACCESS_RIGHTS_SET = set( ["RegistryWrite", "ServiceConnect", "DeviceConnect"] @@ -30,9 +33,9 @@ def _initialize_client(self): if not self.client: if getattr(self.cmd, "cli_ctx", None): self.client = iot_hub_service_factory(self.cmd.cli_ctx) + self.sub_id = get_subscription_id(self.cmd.cli_ctx) else: self.client = self.cmd - self.sub_id = self.client.config.subscription_id def get_iothubs(self, rg: str = None) -> List: self._initialize_client() @@ -44,11 +47,15 @@ def get_iothubs(self, rg: str = None) -> List: else: hubs_pager = self.client.list_by_resource_group(resource_group_name=rg) - try: - while True: - hubs_list.extend(hubs_pager.advance_page()) - except StopIteration: - pass + if ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION): + for hubs in hubs_pager.by_page(): + hubs_list.extend(hubs) + else: + try: + while True: + hubs_list.extend(hubs_pager.advance_page()) + except StopIteration: + pass return hubs_list @@ -60,23 +67,25 @@ def get_policies(self, hub_name: str, rg: str) -> List: ) policy_list = [] - try: - while True: - policy_list.extend(policy_pager.advance_page()) - except StopIteration: - pass + if ensure_iothub_sdk_min_version(IOTHUB_TRACK_2_SDK_MIN_VERSION): + for policy in policy_pager.by_page(): + policy_list.extend(policy) + else: + try: + while True: + policy_list.extend(policy_pager.advance_page()) + except StopIteration: + pass return policy_list def find_iothub(self, hub_name: str, rg: str = None): self._initialize_client() - from azure.mgmt.iothub.models import ErrorDetailsException - if rg: try: return self.client.get(resource_group_name=rg, resource_name=hub_name) - except ErrorDetailsException: + except: # pylint: disable=broad-except raise CLIError( "Unable to find IoT Hub: {} in resource group: {}".format( hub_name, rg @@ -128,12 +137,12 @@ def find_policy(self, hub_name: str, rg: str, policy_name: str = "auto"): def get_target_by_cstring(cls, connection_string: str) -> IotHubTarget: return IotHubTarget.from_connection_string(cstring=connection_string).as_dict() - def get_target(self, hub_name: str, rg: str = None, **kwargs) -> Dict[str, str]: + def get_target(self, hub_name: str, resource_group_name: str = None, **kwargs) -> Dict[str, str]: cstring = kwargs.get("login") if cstring: return self.get_target_by_cstring(connection_string=cstring) - target_iothub = self.find_iothub(hub_name=hub_name, rg=rg) + target_iothub = self.find_iothub(hub_name=hub_name, rg=resource_group_name) policy_name = kwargs.get("policy_name", "auto") rg = target_iothub.additional_properties.get("resourcegroup") @@ -151,13 +160,13 @@ def get_target(self, hub_name: str, rg: str = None, **kwargs) -> Dict[str, str]: include_events=include_events, ) - def get_targets(self, rg: str = None, **kwargs) -> List[Dict[str, str]]: + def get_targets(self, resource_group_name: str = None, **kwargs) -> List[Dict[str, str]]: targets = [] - hubs = self.get_iothubs(rg=rg) + hubs = self.get_iothubs(rg=resource_group_name) if hubs: for hub in hubs: targets.append( - self.get_target(hub_name=hub.name, rg=self._get_rg(hub), **kwargs) + self.get_target(hub_name=hub.name, resource_group_name=self._get_rg(hub), **kwargs) ) return targets @@ -186,7 +195,7 @@ def _build_target( target["subscription"] = self.sub_id target["resourcegroup"] = iothub.additional_properties.get("resourcegroup") target["location"] = iothub.location - target["sku_tier"] = iothub.sku.tier.value + target["sku_tier"] = iothub.sku.tier.value if isinstance(iothub.sku.tier, (Enum, EnumMeta)) else iothub.sku.tier if include_events: events = {} diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 6b270b94c..48afb3481 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -9,6 +9,7 @@ import six from knack.log import get_logger from knack.util import CLIError +from enum import Enum, EnumMeta from azext_iot.constants import ( EXTENSION_ROOT, DEVICE_DEVICESCOPE_PREFIX, @@ -36,7 +37,7 @@ unpack_msrest_error, init_monitoring, process_json_arg, - ensure_min_version, + ensure_iothub_sdk_min_version, generate_key ) from azext_iot._factory import SdkResolver, CloudError @@ -55,7 +56,7 @@ def iot_query( top = _process_top(top) discovery = IotHubDiscovery(cmd) target = discovery.get_target( - hub_name=hub_name, rg=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) @@ -2287,7 +2288,6 @@ def iot_device_export( resource_group_name=None, ): from azext_iot._factory import iot_hub_service_factory - from azure.mgmt.iothub import __version__ as iot_sdk_version client = iot_hub_service_factory(cmd.cli_ctx) discovery = IotHubDiscovery(cmd) @@ -2298,7 +2298,7 @@ def iot_device_export( if exists(blob_container_uri): blob_container_uri = read_file_content(blob_container_uri) - if ensure_min_version(iot_sdk_version, "0.12.0"): + if ensure_iothub_sdk_min_version("0.12.0"): from azure.mgmt.iothub.models import ExportDevicesRequest from azext_iot.common.shared import AuthenticationType @@ -2336,7 +2336,6 @@ def iot_device_import( resource_group_name=None, ): from azext_iot._factory import iot_hub_service_factory - from azure.mgmt.iothub import __version__ as iot_sdk_version client = iot_hub_service_factory(cmd.cli_ctx) discovery = IotHubDiscovery(cmd) @@ -2350,7 +2349,7 @@ def iot_device_import( if exists(output_blob_container_uri): output_blob_container_uri = read_file_content(output_blob_container_uri) - if ensure_min_version(iot_sdk_version, "0.12.0"): + if ensure_iothub_sdk_min_version("0.12.0"): from azure.mgmt.iothub.models import ImportDevicesRequest from azext_iot.common.shared import AuthenticationType @@ -2711,7 +2710,7 @@ def _get_hub_connection_string( entityPath, ) for p in policies - if "serviceconnect" in p.rights.value.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/iothub/jobs/test_iothub_jobs_int.py b/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py index 1bd76bbcc..902adcfd9 100644 --- a/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py +++ b/azext_iot/tests/iothub/jobs/test_iothub_jobs_int.py @@ -182,6 +182,21 @@ def test_jobs(self): 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 diff --git a/azext_iot/tests/iothub/test_iothub_discovery_int.py b/azext_iot/tests/iothub/test_iothub_discovery_int.py index b0bd35821..1f83be955 100644 --- a/azext_iot/tests/iothub/test_iothub_discovery_int.py +++ b/azext_iot/tests/iothub/test_iothub_discovery_int.py @@ -74,7 +74,7 @@ def test_iothub_targets(self): auto_target = discovery.get_target(hub_name=LIVE_HUB) assert_target(auto_target, rg=LIVE_RG) - auto_target = discovery.get_target(hub_name=LIVE_HUB, rg=LIVE_RG) + auto_target = discovery.get_target(hub_name=LIVE_HUB, resource_group_name=LIVE_RG) assert_target(auto_target, rg=LIVE_RG) desired_target = discovery.get_target( @@ -85,7 +85,7 @@ def test_iothub_targets(self): sub_targets = discovery.get_targets() [assert_target(tar) for tar in sub_targets] - rg_targets = discovery.get_targets(rg=LIVE_RG, include_events=True) + rg_targets = discovery.get_targets(resource_group_name=LIVE_RG, include_events=True) [assert_target(tar, rg=LIVE_RG, include_events=True) for tar in rg_targets] assert len(rg_targets) <= len(sub_targets) @@ -99,7 +99,7 @@ def assert_target(target: dict, by_cstring=False, include_events=False, **kwargs if not by_cstring: assert target["secondarykey"] - assert target["subscription"] + assert target["subscription"] and target["subscription"] != "unknown" if "rg" in kwargs: assert target["resourcegroup"] == kwargs["rg"] diff --git a/azext_iot/tests/utility/test_iot_utility_unit.py b/azext_iot/tests/utility/test_iot_utility_unit.py index 62516539e..df943f6d8 100644 --- a/azext_iot/tests/utility/test_iot_utility_unit.py +++ b/azext_iot/tests/utility/test_iot_utility_unit.py @@ -16,7 +16,7 @@ process_json_arg, read_file_content, logger, - ensure_min_version, + ensure_iothub_sdk_min_version, ) from azext_iot.common.deps import ensure_uamqp from azext_iot.constants import EVENT_LIB, EXTENSION_NAME @@ -294,8 +294,13 @@ class TestVersionComparison(object): ("2.0.1.9", "2.0.6", False), ], ) - def test_ensure_min_version(self, current, minimum, expected): - assert ensure_min_version(current, minimum) == expected + def test_ensure_iothub_sdk_min_version(self, mocker, current, minimum, expected): + try: + mocker.patch("azure.mgmt.iothub.__version__", current) + except: + mocker.patch("azure.mgmt.iothub._configuration.VERSION", current) + + assert ensure_iothub_sdk_min_version(minimum) == expected class TestEmbeddedCli(object): diff --git a/setup.py b/setup.py index f5397799c..23833446c 100644 --- a/setup.py +++ b/setup.py @@ -41,14 +41,14 @@ # 'jmespath==0.9.3', # 'pyyaml==3.13' # 'knack>=0.3.1' -# 'jsonschema==3.0.2' +# 'jsonschema==3.2.0' # 'enum34' (when python_version < 3.4) # There is also a dependency for uamqp for amqp based commands # though that is installed out of band (managed by the extension) # for compatibility reasons. -DEPENDENCIES = ["paho-mqtt==1.5.0", "jsonschema==3.2.0", "setuptools"] +DEPENDENCIES = ["paho-mqtt==1.5.0", "jsonschema==3.2.0", "packaging"] CLASSIFIERS = [ From 0b22a46eb7b1c7fe48e97d19c235bcd9391a62cf Mon Sep 17 00:00:00 2001 From: Paymaun Date: Mon, 26 Apr 2021 12:01:57 -0700 Subject: [PATCH 179/179] Fix a couple issues for v0.10.11 (#333) * Tweak adt api interaction sleep time. --- HISTORY.rst | 28 +++++++++++-- azext_iot/constants.py | 2 +- azext_iot/digitaltwins/providers/twin.py | 12 ++++-- azext_iot/operations/hub.py | 32 +++++++-------- .../service/operations/devices_operations.py | 4 +- .../service/operations/modules_operations.py | 4 +- .../test_dt_twin_lifecycle_int.py | 5 ++- .../tests/digitaltwins/test_dt_twins_unit.py | 41 +++++++++++++++++++ azext_iot/tests/iothub/test_iot_ext_unit.py | 6 ++- 9 files changed, 104 insertions(+), 30 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c010181de..0b3aa6a3c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,30 @@ Release History =============== +0.10.11 ++++++++++++++++ + +**IoT Hub updates** + +* Fixed an issue where an explicit json null could not be sent for the following commands: + + * az iot hub invoke-device-method + * az iot hub invoke-module-method + +**Azure Digital Twins updates** + +* Fixed an issue in the following update commands where malformed json patch content would not raise an error + causing the process to call the respective service endpoint with a request payload containing an empty array. + + * az dt twin update + * az dt twin relationship update + * az dt twin component update + +**IoT Central updates** + +Placeholder + + 0.10.10 +++++++++++++++ @@ -18,10 +42,6 @@ Release History * az dt twin create * az dt twin relationship create -**IoT Central updates** - -Placeholder - 0.10.9 +++++++++++++++ diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 7ed91cbc5..d4c932367 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.10.10" +VERSION = "0.10.11" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" diff --git a/azext_iot/digitaltwins/providers/twin.py b/azext_iot/digitaltwins/providers/twin.py index e07bf4c92..e465a355d 100644 --- a/azext_iot/digitaltwins/providers/twin.py +++ b/azext_iot/digitaltwins/providers/twin.py @@ -89,8 +89,10 @@ def update(self, twin_id, json_patch, etag=None): json_patch_collection = [] if isinstance(json_patch, dict): json_patch_collection.append(json_patch) - if isinstance(json_patch, list): + elif isinstance(json_patch, list): json_patch_collection.extend(json_patch) + else: + raise CLIError(f"--json-patch content must be an object or array. Actual type was: {type(json_patch).__name__}") logger.info("Patch payload %s", json.dumps(json_patch_collection)) @@ -202,8 +204,10 @@ def update_relationship(self, twin_id, relationship_id, json_patch, etag=None): json_patch_collection = [] if isinstance(json_patch, dict): json_patch_collection.append(json_patch) - if isinstance(json_patch, list): + elif isinstance(json_patch, list): json_patch_collection.extend(json_patch) + else: + raise CLIError(f"--json-patch content must be an object or array. Actual type was: {type(json_patch).__name__}") logger.info("Patch payload %s", json.dumps(json_patch_collection)) @@ -272,8 +276,10 @@ def update_component(self, twin_id, component_path, json_patch, etag=None): json_patch_collection = [] if isinstance(json_patch, dict): json_patch_collection.append(json_patch) - if isinstance(json_patch, list): + elif isinstance(json_patch, list): json_patch_collection.extend(json_patch) + else: + raise CLIError(f"--json-patch content must be an object or array. Actual type was: {type(json_patch).__name__}") logger.info("Patch payload %s", json.dumps(json_patch_collection)) diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 48afb3481..2a234aa53 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -1502,7 +1502,6 @@ def iot_device_method( resource_group_name=None, login=None, ): - from azext_iot.sdk.iothub.service.models import CloudToDeviceMethod from azext_iot.constants import ( METHOD_INVOKE_MAX_TIMEOUT_SEC, METHOD_INVOKE_MIN_TIMEOUT_SEC, @@ -1532,14 +1531,15 @@ def iot_device_method( method_payload, argument_name="method-payload" ) - method = CloudToDeviceMethod( - method_name=method_name, - response_timeout_in_seconds=timeout, - connect_timeout_in_seconds=timeout, - payload=method_payload, - ) + request_body = { + "methodName": method_name, + "payload": method_payload, + "responseTimeoutInSeconds": timeout, + "connectTimeoutInSeconds": timeout, + } + return service_sdk.devices.invoke_method( - device_id=device_id, direct_method_request=method, timeout=timeout + device_id=device_id, direct_method_request=request_body, timeout=timeout, ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) @@ -1559,7 +1559,6 @@ def iot_device_module_method( resource_group_name=None, login=None, ): - from azext_iot.sdk.iothub.service.models import CloudToDeviceMethod from azext_iot.constants import ( METHOD_INVOKE_MAX_TIMEOUT_SEC, METHOD_INVOKE_MIN_TIMEOUT_SEC, @@ -1589,16 +1588,17 @@ def iot_device_module_method( method_payload, argument_name="method-payload" ) - method = CloudToDeviceMethod( - method_name=method_name, - response_timeout_in_seconds=timeout, - connect_timeout_in_seconds=timeout, - payload=method_payload, - ) + request_body = { + "methodName": method_name, + "payload": method_payload, + "responseTimeoutInSeconds": timeout, + "connectTimeoutInSeconds": timeout, + } + return service_sdk.modules.invoke_method( device_id=device_id, module_id=module_id, - direct_method_request=method, + direct_method_request=request_body, timeout=timeout, ) except CloudError as e: diff --git a/azext_iot/sdk/iothub/service/operations/devices_operations.py b/azext_iot/sdk/iothub/service/operations/devices_operations.py index 487a90a18..086e7607e 100644 --- a/azext_iot/sdk/iothub/service/operations/devices_operations.py +++ b/azext_iot/sdk/iothub/service/operations/devices_operations.py @@ -534,7 +534,9 @@ def invoke_method( header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') # Construct body - body_content = self._serialize.body(direct_method_request, 'CloudToDeviceMethod') + # @digimaun - Originally 'CloudToDeviceMethod'. Model serialization forces a null payload property to be removed. + # TODO: Test model behavior in latest autorest generator. + body_content = self._serialize.body(direct_method_request, 'object') # Construct and send request request = self._client.post(url, query_parameters, header_parameters, body_content) diff --git a/azext_iot/sdk/iothub/service/operations/modules_operations.py b/azext_iot/sdk/iothub/service/operations/modules_operations.py index 338c512b5..2f4ecf663 100644 --- a/azext_iot/sdk/iothub/service/operations/modules_operations.py +++ b/azext_iot/sdk/iothub/service/operations/modules_operations.py @@ -550,7 +550,9 @@ def invoke_method( header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') # Construct body - body_content = self._serialize.body(direct_method_request, 'CloudToDeviceMethod') + # @digimaun - Originally 'CloudToDeviceMethod'. Model serialization forces a null payload property to be removed. + # TODO: Test model behavior in latest autorest generator. + body_content = self._serialize.body(direct_method_request, 'object') # Construct and send request request = self._client.post(url, query_parameters, header_parameters, body_content) 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 7f6f77d35..e7f0fc38b 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py +++ b/azext_iot/tests/digitaltwins/test_dt_twin_lifecycle_int.py @@ -506,7 +506,7 @@ def test_dt_twin(self): instance_name, ) ) - sleep(10) # Wait for API to catch up + sleep(5) # Wait for API to catch up twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins' --cost".format( @@ -586,6 +586,7 @@ def test_dt_twin_delete(self): component_name=thermostat_component_id, ) + sleep(5) # Wait for API to catch up twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins'".format( instance_name, self.rg @@ -674,7 +675,7 @@ def test_dt_twin_delete(self): instance_name, ) ) - sleep(10) # Wait for API to catch up + sleep(5) # Wait for API to catch up twin_query_result = self.cmd( "dt twin query -n {} -g {} -q 'select * from digitaltwins' --cost".format( diff --git a/azext_iot/tests/digitaltwins/test_dt_twins_unit.py b/azext_iot/tests/digitaltwins/test_dt_twins_unit.py index f5237f70e..dfaada441 100644 --- a/azext_iot/tests/digitaltwins/test_dt_twins_unit.py +++ b/azext_iot/tests/digitaltwins/test_dt_twins_unit.py @@ -436,6 +436,19 @@ def test_update_twin(self, fixture_cmd, service_client, json_patch, resource_gro assert result == generate_twin_result() + def test_update_twin_invalid_patch(self, fixture_cmd, service_client): + # CLIError is raised when --json-patch is not dict or list + with pytest.raises(CLIError) as e: + subject.update_twin( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + json_patch="'{op:add,path:/setPointTemp,value:50.2}'", + resource_group_name=None, + etag=None + ) + assert str(e.value) == "--json-patch content must be an object or array. Actual type was: str" + @pytest.fixture(params=[(400, 200), (401, 200), (500, 200), (202, 400), (202, 401), (202, 500)]) def service_client_error(self, mocked_response, start_twin_response, request): mocked_response.add( @@ -845,6 +858,20 @@ def test_update_relationship(self, fixture_cmd, service_client, json_patch, reso assert result == json.loads(generic_result) + def test_update_relationship_invalid_patch(self, fixture_cmd, service_client): + # CLIError is raised when --json-patch is not dict or list + with pytest.raises(CLIError) as e: + subject.update_relationship( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + relationship_id=relationship_id, + json_patch="'{op:add,path:/setPointTemp,value:50.2}'", + resource_group_name=None, + etag=None + ) + assert str(e.value) == "--json-patch content must be an object or array. Actual type was: str" + @pytest.fixture(params=[(400, 200), (401, 200), (500, 200), (204, 400), (204, 401), (204, 500)]) def service_client_error(self, mocked_response, start_twin_response, request): mocked_response.add( @@ -1514,6 +1541,20 @@ def test_update_component(self, fixture_cmd, service_client, json_patch, resourc assert result == json.loads(generic_result) + def test_update_component_invalid_patch(self, fixture_cmd, service_client): + # CLIError is raised when --json-patch is not dict or list + with pytest.raises(CLIError) as e: + subject.update_component( + cmd=fixture_cmd, + name_or_hostname=hostname, + twin_id=twin_id, + component_path=component_path, + json_patch="'{op:add,path:/setPointTemp,value:50.2}'", + resource_group_name=None, + etag=None + ) + assert str(e.value) == "--json-patch content must be an object or array. Actual type was: str" + @pytest.fixture(params=[400, 401, 500]) def service_client_error(self, mocked_response, start_twin_response, request): mocked_response.add( diff --git a/azext_iot/tests/iothub/test_iot_ext_unit.py b/azext_iot/tests/iothub/test_iot_ext_unit.py index 7f701a442..51fb2ada9 100644 --- a/azext_iot/tests/iothub/test_iot_ext_unit.py +++ b/azext_iot/tests/iothub/test_iot_ext_unit.py @@ -1578,7 +1578,8 @@ def test_device_method(self, serviceclient, methodbody): if methodbody: assert body["payload"] == json.loads(payload) else: - assert body.get("payload") is None + # We must ensure null is passed for payload. + assert body["payload"] is None assert body["responseTimeoutInSeconds"] == timeout assert body["connectTimeoutInSeconds"] == timeout @@ -1658,7 +1659,8 @@ def test_device_module_method(self, serviceclient, methodbody): if methodbody: assert body["payload"] == json.loads(payload) else: - assert body.get("payload") is None + # We must ensure null is passed for payload. + assert body["payload"] is None assert body["responseTimeoutInSeconds"] == timeout assert body["connectTimeoutInSeconds"] == timeout