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

[IoT] az iot hub create\update: File upload and certificate updates #18966

Merged
merged 10 commits into from
Jul 29, 2021
3 changes: 2 additions & 1 deletion src/azure-cli-core/azure/cli/core/profiles/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class ResourceType(Enum): # pylint: disable=too-few-public-methods
DATA_KEYVAULT_ADMINISTRATION_ACCESS_CONTROL = ('azure.keyvault.administration', 'KeyVaultAccessControlClient')
MGMT_EVENTHUB = ('azure.mgmt.eventhub', 'EventHubManagementClient')
MGMT_APPSERVICE = ('azure.mgmt.web', 'WebSiteManagementClient')
MGMT_IOTCENTRAL = ('azure.mgmt.iotcentral', 'IotCentralClient')
MGMT_IOTHUB = ('azure.mgmt.iothub', 'IotHubClient')
MGMT_ARO = ('azure.mgmt.redhatopenshift', 'AzureRedHatOpenShiftClient')
MGMT_DATABOXEDGE = ('azure.mgmt.databoxedge', 'DataBoxEdgeManagementClient')
Expand Down Expand Up @@ -83,7 +84,6 @@ class ResourceType(Enum): # pylint: disable=too-few-public-methods
MGMT_DATALAKE_STORE = ('azure.mgmt.datalake.store', None)
MGMT_DATAMIGRATION = ('azure.mgmt.datamigration', None)
MGMT_EVENTGRID = ('azure.mgmt.eventgrid', None)
MGMT_IOTCENTRAL = ('azure.mgmt.iotcentral', None)
MGMT_DEVTESTLABS = ('azure.mgmt.devtestlabs', None)
MGMT_MAPS = ('azure.mgmt.maps', None)
MGMT_POLICYINSIGHTS = ('azure.mgmt.policyinsights', None)
Expand Down Expand Up @@ -216,6 +216,7 @@ def default_api_version(self):
}),
ResourceType.MGMT_APPSERVICE: '2020-09-01',
ResourceType.MGMT_IOTHUB: '2021-03-31',
ResourceType.MGMT_IOTCENTRAL: '2018-09-01',
ResourceType.MGMT_ARO: '2020-04-30',
ResourceType.MGMT_DATABOXEDGE: '2019-08-01',
ResourceType.MGMT_CUSTOMLOCATION: '2021-03-15-preview',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ def resource_service_factory(cli_ctx, **_):

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
from azure.mgmt.iothubprovisioningservices import IotDpsClient
return get_mgmt_service_client(cli_ctx, IotDpsClient)


def iot_central_service_factory(cli_ctx, *_):
from azure.cli.core.commands.client_factory import get_mgmt_service_client
from azure.mgmt.iotcentral import IotCentralClient
return get_mgmt_service_client(cli_ctx, IotCentralClient)
from azure.cli.core.profiles import ResourceType
return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_IOTCENTRAL)
6 changes: 3 additions & 3 deletions src/azure-cli/azure/cli/command_modules/iot/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@
type: command
short-summary: Update metadata for an IoT hub.
examples:
- name: Add a storage container settings
- name: Add storage container settings to file upload
text: >
az iot hub update --name MyIotHub --fileupload-storage-connectionstring "connection-string" \\
--fileupload-storage-container-name "container_name"
Expand Down Expand Up @@ -826,10 +826,10 @@
- name: Update the IoT Hub file upload settings, and assign a managed identity to user for file upload
text: >
az iot hub update -n MyIoTHub --fileupload-sas-ttl 5 --fileupload-storage-auth-type identityBased --fileupload-storage-identity [system]
- name: Update the IoT Hub file upload notification settings
- name: Update the IoT Hub file upload notification settings and queue lock duration
text: >
az iot hub update -n MyIoTHub --fileupload-notification-max-delivery-count 50
--fileupload-notification-ttl 48 --fileupload-notifications
--fileupload-notification-ttl 48 --fileupload-notifications --fileupload-notification-lock-duration 10
"""

helps['iot central'] = """
Expand Down
8 changes: 7 additions & 1 deletion src/azure-cli/azure/cli/command_modules/iot/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
validate_fileupload_sas_ttl,
validate_feedback_ttl,
validate_feedback_lock_duration,
validate_fileupload_notification_lock_duration,
validate_feedback_max_delivery_count,
validate_c2d_max_delivery_count,
validate_c2d_ttl)
Expand Down Expand Up @@ -151,6 +152,10 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
arg_type=get_three_state_flag(),
help='A boolean indicating whether to log information about uploaded files to the'
' messages/servicebound/filenotifications IoT Hub endpoint.')
c.argument('fileupload_notification_lock_duration',
options_list=['--fileupload-notification-lock-duration', '--fnld'],
type=int, validator=validate_fileupload_notification_lock_duration,
help='The lock duration for the file upload notifications queue, between 5 and 300 seconds.')
c.argument('fileupload_notification_max_delivery_count', type=int,
options_list=['--fileupload-notification-max-delivery-count', '--fnd'],
validator=validate_fileupload_notification_max_delivery_count,
Expand All @@ -169,7 +174,8 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
'Possible values are keyBased and identityBased')
c.argument('fileupload_storage_container_uri',
options_list=['--fileupload-storage-container-uri', '--fcu'],
help='The container URI for the Azure Storage account to which files are uploaded.')
help='The container URI for the Azure Storage account to which files are uploaded.',
deprecate_info=c.deprecate(hide=True))
c.argument('fileupload_storage_container_name',
options_list=['--fileupload-storage-container-name', '--fc'],
help='The name of the root container where you upload files. The container need not exist but'
Expand Down
5 changes: 1 addition & 4 deletions src/azure-cli/azure/cli/command_modules/iot/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ def open_certificate(certificate_path):
if certificate_path.endswith('.pem') or certificate_path.endswith('.cer'):
with open(certificate_path, "rb") as cert_file:
certificate = cert_file.read()
try:
certificate = certificate.decode("utf-8")
except UnicodeError:
certificate = base64.b64encode(certificate).decode("utf-8")
certificate = base64.b64encode(certificate).decode("utf-8")
c-ryan-k marked this conversation as resolved.
Show resolved Hide resolved
return certificate


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ def validate_feedback_ttl(ns):
def validate_feedback_lock_duration(ns):
if (ns.feedback_lock_duration and
ns.feedback_lock_duration not in range(5, 301, 1)):
raise ArgumentError(None, 'Please specify the duration from 5 to 300 seconds only.')
raise ArgumentError(None, 'Please specify the feedback lock duration from 5 to 300 seconds only.')
c-ryan-k marked this conversation as resolved.
Show resolved Hide resolved


def validate_fileupload_notification_lock_duration(ns):
if (ns.fileupload_notification_lock_duration and
ns.fileupload_notification_lock_duration not in range(5, 301, 1)):
raise ArgumentError(None, 'Please specify the notification lock duration from 5 to 300 seconds only.')
c-ryan-k marked this conversation as resolved.
Show resolved Hide resolved


def validate_feedback_max_delivery_count(ns):
Expand Down
51 changes: 42 additions & 9 deletions src/azure-cli/azure/cli/command_modules/iot/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ def iot_hub_create(cmd, client, hub_name, resource_group_name, location=None,
feedback_ttl=1,
feedback_max_delivery_count=10,
enable_fileupload_notifications=False,
fileupload_notification_lock_duration=5,
fileupload_notification_max_delivery_count=10,
fileupload_notification_ttl=1,
fileupload_storage_connectionstring=None,
Expand All @@ -425,13 +426,13 @@ def iot_hub_create(cmd, client, hub_name, resource_group_name, location=None,
if fileupload_storage_container_name and not fileupload_storage_connectionstring:
raise RequiredArgumentMissingError('Please mention storage connection string.')
identity_based_file_upload = fileupload_storage_authentication_type and fileupload_storage_authentication_type.lower() == AuthenticationType.IdentityBased.value
if not identity_based_file_upload and not fileupload_storage_connectionstring and fileupload_storage_container_name:
raise RequiredArgumentMissingError('Key-based authentication requires a connection string.')
if identity_based_file_upload and not fileupload_storage_container_uri:
raise RequiredArgumentMissingError('Identity-based authentication requires a storage container uri (--fileupload-storage-container-uri, --fcu).')
if not identity_based_file_upload and fileupload_storage_identity:
raise RequiredArgumentMissingError('In order to set a fileupload storage identity, please set file upload storage authentication (--fsa) to IdentityBased')

if identity_based_file_upload or fileupload_storage_identity:
if fileupload_storage_identity == SYSTEM_ASSIGNED_IDENTITY and not system_identity:
c-ryan-k marked this conversation as resolved.
Show resolved Hide resolved
raise ArgumentUsageError('System managed identity [--mi-system-assigned] must be enabled in order to use managed identity for file upload')
if fileupload_storage_identity and fileupload_storage_identity != SYSTEM_ASSIGNED_IDENTITY and not user_identities:
raise ArgumentUsageError('User identity [--mi-user-assigned] must be added in order to use it for file upload')
location = _ensure_location(cli_ctx, resource_group_name, location)
sku = IotHubSkuInfo(name=sku, capacity=unit)

Expand All @@ -446,7 +447,8 @@ def iot_hub_create(cmd, client, hub_name, resource_group_name, location=None,
feedback=feedback_Properties)
msg_endpoint_dic = {}
msg_endpoint_dic['fileNotifications'] = MessagingEndpointProperties(max_delivery_count=fileupload_notification_max_delivery_count,
ttl_as_iso8601=timedelta(hours=fileupload_notification_ttl))
ttl_as_iso8601=timedelta(hours=fileupload_notification_ttl),
lock_duration_as_iso8601=timedelta(seconds=fileupload_notification_lock_duration))
storage_endpoint_dic = {}
storage_endpoint_dic['$default'] = StorageEndpointProperties(
sas_ttl_as_iso8601=timedelta(hours=fileupload_sas_ttl),
Expand Down Expand Up @@ -521,6 +523,7 @@ def update_iot_hub_custom(instance,
feedback_ttl=None,
feedback_max_delivery_count=None,
enable_fileupload_notifications=None,
fileupload_notification_lock_duration=None,
fileupload_notification_max_delivery_count=None,
fileupload_notification_ttl=None,
fileupload_storage_connectionstring=None,
Expand Down Expand Up @@ -552,12 +555,33 @@ def update_iot_hub_custom(instance,
instance.properties.cloud_to_device.feedback.max_delivery_count = feedback_max_delivery_count
if enable_fileupload_notifications is not None:
instance.properties.enable_file_upload_notifications = enable_fileupload_notifications
if fileupload_notification_lock_duration is not None:
lock_duration = timedelta(seconds=fileupload_notification_lock_duration)
instance.properties.messaging_endpoints['fileNotifications'].lock_duration_as_iso8601 = lock_duration
if fileupload_notification_max_delivery_count is not None:
count = fileupload_notification_max_delivery_count
instance.properties.messaging_endpoints['fileNotifications'].max_delivery_count = count
if fileupload_notification_ttl is not None:
ttl = timedelta(hours=fileupload_notification_ttl)
instance.properties.messaging_endpoints['fileNotifications'].ttl_as_iso8601 = ttl
# if setting a fileupload storage identity or changing fileupload to identity-based
if fileupload_storage_identity or (fileupload_storage_authentication_type and fileupload_storage_authentication_type.lower() == AuthenticationType.IdentityBased.value):
c-ryan-k marked this conversation as resolved.
Show resolved Hide resolved
instance_identity = _get_hub_identity_type(instance)

# if hub has no identity
if not instance_identity or instance_identity == IdentityType.none.value:
raise ArgumentUsageError('Hub has no identity assigned, please assign a system or user-assigned managed identity to use for file-upload with `az iot hub identity assign`')

has_system_identity = instance_identity in [IdentityType.system_assigned.value, IdentityType.system_assigned_user_assigned.value]
has_user_identity = instance_identity in [IdentityType.user_assigned.value, IdentityType.system_assigned_user_assigned.value]

# if changing storage identity to '[system]'
if fileupload_storage_identity == SYSTEM_ASSIGNED_IDENTITY:
if not has_system_identity:
raise ArgumentUsageError('System managed identity must be enabled in order to use managed identity for file upload')
# if changing to user identity and hub has no user identities
elif fileupload_storage_identity and not has_user_identity:
raise ArgumentUsageError('User identity {} must be added to hub in order to use it for file upload'.format(fileupload_storage_identity))

default_storage_endpoint = _process_fileupload_args(
instance.properties.storage_endpoints['$default'],
Expand Down Expand Up @@ -1287,8 +1311,12 @@ def _process_fileupload_args(
):
from datetime import timedelta
if fileupload_storage_authentication_type and fileupload_storage_authentication_type.lower() == AuthenticationType.IdentityBased.value:
default_storage_endpoint.authentication_type = AuthenticationType.IdentityBased
default_storage_endpoint.container_uri = fileupload_storage_container_uri
default_storage_endpoint.authentication_type = AuthenticationType.IdentityBased.value
if fileupload_storage_container_uri:
default_storage_endpoint.container_uri = fileupload_storage_container_uri
elif fileupload_storage_authentication_type and fileupload_storage_authentication_type.lower() == AuthenticationType.KeyBased.value:
default_storage_endpoint.authentication_type = AuthenticationType.KeyBased.value
default_storage_endpoint.identity = None
elif fileupload_storage_authentication_type is not None:
default_storage_endpoint.authentication_type = None
default_storage_endpoint.container_uri = None
Expand All @@ -1306,7 +1334,7 @@ def _process_fileupload_args(
# Fix for identity/authentication-type params missing on hybrid profile api
if hasattr(default_storage_endpoint, 'authentication_type'):
# If we are now (or will be) using fsa=identity AND we've set a new identity
if default_storage_endpoint.authentication_type == AuthenticationType.IdentityBased and fileupload_storage_identity:
if default_storage_endpoint.authentication_type and default_storage_endpoint.authentication_type.lower() == AuthenticationType.IdentityBased.value and fileupload_storage_identity:
# setup new fsi
default_storage_endpoint.identity = ManagedIdentity(
user_assigned_identity=fileupload_storage_identity) if fileupload_storage_identity not in [IdentityType.none.value, SYSTEM_ASSIGNED_IDENTITY] else None
Expand All @@ -1317,6 +1345,11 @@ def _process_fileupload_args(
return default_storage_endpoint


def _get_hub_identity_type(instance):
identity = getattr(instance, 'identity', {})
return getattr(identity, 'type', None)


def _build_identity(system=False, identities=None):
identity_type = IdentityType.none.value
if not (system or identities):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def _create_test_cert(cert_file, key_file, subject, valid_days, serial_number):
cert.set_pubkey(k)
cert.sign(k, 'sha256')

cert_str = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('ascii')
key_str = crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode('ascii')
cert_str = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')
key_str = crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode('utf-8')

open(cert_file, 'w').write(cert_str)
open(key_file, 'w').write(key_str)
Expand Down
Loading