Skip to content

Commit

Permalink
Added ability to retrieve device provisioning state from central and …
Browse files Browse the repository at this point in the history
…underlying DPS (#167)

* Added ability to retrieve device provisioning state from central and underlying DPS
  • Loading branch information
prbans authored Apr 21, 2020
1 parent b563743 commit e5039cd
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 20 deletions.
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

0 comments on commit e5039cd

Please sign in to comment.