Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to retrieve device provisioning state from central and underlying DPS #167

Merged
merged 2 commits into from Apr 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions azext_iot/central/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions azext_iot/central/command_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 33 additions & 7 deletions azext_iot/central/commands_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand All @@ -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,
Expand All @@ -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
)
31 changes: 22 additions & 9 deletions azext_iot/central/commands_device_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand All @@ -35,14 +43,15 @@ 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):
raise CLIError("content must be a string: {}".format(content))

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,
Expand All @@ -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
)
8 changes: 8 additions & 0 deletions azext_iot/central/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
)
74 changes: 73 additions & 1 deletion azext_iot/central/providers/device_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
30 changes: 30 additions & 0 deletions azext_iot/central/services/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
5 changes: 5 additions & 0 deletions azext_iot/dps/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
# --------------------------------------------------------------------------------------------
5 changes: 5 additions & 0 deletions azext_iot/dps/services/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
# --------------------------------------------------------------------------------------------
28 changes: 28 additions & 0 deletions azext_iot/dps/services/auth.py
Original file line number Diff line number Diff line change
@@ -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
Loading