diff --git a/azext_edge/edge/commands_edge.py b/azext_edge/edge/commands_edge.py index 17808ed78..140320539 100644 --- a/azext_edge/edge/commands_edge.py +++ b/azext_edge/edge/commands_edge.py @@ -11,7 +11,6 @@ from .providers.base import load_config_context from .providers.support.base import get_bundle_path -from .common import DeployablePasVersions from .providers.check.common import ResourceOutputDetailLevel logger = get_logger(__name__) @@ -69,7 +68,6 @@ def init( resource_group_name: str, cluster_namespace: str = "default", custom_location_namespace: Optional[str] = None, - pas_version: str = DeployablePasVersions.v012.value, custom_location_name: Optional[str] = None, show_pas_version: Optional[bool] = None, custom_version: Optional[List[str]] = None, @@ -80,7 +78,7 @@ def init( simulate_plc: Optional[bool] = None, opcua_discovery_endpoint: Optional[str] = None, create_sync_rules: Optional[bool] = None, - block: Union[bool, str] = "true", + no_block: Optional[bool] = None, no_progress: Optional[bool] = None, processor_instance_name: Optional[str] = None, target_name: Optional[str] = None, @@ -116,7 +114,6 @@ def init( custom_location_name=custom_location_name, custom_location_namespace=custom_location_namespace, resource_group_name=resource_group_name, - pas_version=pas_version, location=location, show_pas_version=show_pas_version, custom_version=custom_version, @@ -126,7 +123,7 @@ def init( opcua_discovery_endpoint=opcua_discovery_endpoint, simulate_plc=simulate_plc, create_sync_rules=create_sync_rules, - block=block, + no_block=no_block, no_progress=no_progress, processor_instance_name=processor_instance_name, target_name=target_name, diff --git a/azext_edge/edge/params.py b/azext_edge/edge/params.py index 9b7c8f19d..f3b5fab8c 100644 --- a/azext_edge/edge/params.py +++ b/azext_edge/edge/params.py @@ -11,7 +11,7 @@ from azure.cli.core.commands.parameters import get_three_state_flag, get_enum_type from knack.arguments import CaseInsensitiveList -from .common import DeployablePasVersions, SupportForEdgeServiceType +from .common import SupportForEdgeServiceType from .providers.edge_api import E4kResourceKinds from .providers.orchestration.pas_versions import EdgeServiceMoniker from .providers.check.common import ResourceOutputDetailLevel @@ -38,7 +38,7 @@ def load_iotedge_arguments(self, _): help="K8s cluster namespace the command should operate against. " "If no namespace is provided the kubeconfig current_context namespace will be used. " "If not defined, the fallback value `default` will be used. ", - validator=validate_namespace + validator=validate_namespace, ) with self.argument_context("edge support") as context: @@ -230,16 +230,9 @@ def load_iotedge_arguments(self, _): help="Flag when set, will output the generated template intended for deployment.", arg_group="Template", ) - context.argument( - "pas_version", - options_list=["--pas-version"], - help="The PAS bundle version to deploy.", - choices=CaseInsensitiveList(DeployablePasVersions.list()), - arg_group="PAS Version", - ) context.argument( "show_pas_version", - options_list=["--show-version"], + options_list=["--pas-version"], help="Summarize and show the versions of deployable components.", arg_type=get_three_state_flag(), arg_group="PAS Version", @@ -247,19 +240,21 @@ def load_iotedge_arguments(self, _): context.argument( "custom_version", nargs="+", - options_list=["--custom-version"], + options_list=[context.deprecate(hide=True, target="--custom-version")], help="Customize PAS deployment by specifying edge service versions. Usage takes " "precedence over --aio-version. Use space-separated {key}={value} pairs where {key} " "is the edge service moniker and {value} is the desired version. The following monikers " f"may be used: {', '.join(EdgeServiceMoniker.list())}. Example: e4k=0.5.0 bluefin=0.3.0", arg_group="PAS Version", + deprecate_info=context.deprecate(hide=True), ) context.argument( "only_deploy_custom", - options_list=["--only-custom"], + options_list=[context.deprecate(hide=True, target="--only-custom")], arg_type=get_three_state_flag(), help="Only deploy the edge services specified in --custom-version.", arg_group="PAS Version", + deprecate_info=context.deprecate(hide=True), ) context.argument( "create_sync_rules", @@ -274,10 +269,10 @@ def load_iotedge_arguments(self, _): help="Disable deployment progress bar.", ) context.argument( - "block", - options_list=["--block"], + "no_block", + options_list=["--no-block"], arg_type=get_three_state_flag(), - help="Determines whether the operation should block for completion.", + help="Disable blocking until completion.", ) # Akri context.argument( diff --git a/azext_edge/edge/providers/base.py b/azext_edge/edge/providers/base.py index a1bd4cb9d..48be9dd87 100644 --- a/azext_edge/edge/providers/base.py +++ b/azext_edge/edge/providers/base.py @@ -27,6 +27,11 @@ def load_config_context(context_name: Optional[str] = None): """ Load default config using a specific context or 'current-context' if not specified. """ + from ..util import set_log_level + + # This will ensure --debug works with http(s) k8s interactions + set_log_level("urllib3.connectionpool") + config.load_kube_config(context=context_name) _, current_config = config.list_kube_config_contexts() global DEFAULT_NAMESPACE diff --git a/azext_edge/edge/providers/orchestration/__init__.py b/azext_edge/edge/providers/orchestration/__init__.py index b8b9c3efd..03a3789fc 100644 --- a/azext_edge/edge/providers/orchestration/__init__.py +++ b/azext_edge/edge/providers/orchestration/__init__.py @@ -12,6 +12,7 @@ EdgeExtensionName, extension_name_to_type_map, moniker_to_extension_type_map, + DEPLOYABLE_PAS_VERSION, ) @@ -23,4 +24,5 @@ "EdgeExtensionName", "extension_name_to_type_map", "moniker_to_extension_type_map", + "DEPLOYABLE_PAS_VERSION", ] diff --git a/azext_edge/edge/providers/orchestration/base.py b/azext_edge/edge/providers/orchestration/base.py index 7a56f7524..d655c22b5 100644 --- a/azext_edge/edge/providers/orchestration/base.py +++ b/azext_edge/edge/providers/orchestration/base.py @@ -12,6 +12,7 @@ get_pas_version_def, extension_name_to_type_map, EdgeExtensionName, + DEPLOYABLE_PAS_VERSION, ) from ...util import get_timestamp_now_utc @@ -267,7 +268,6 @@ def deploy( resource_group_name: str, custom_location_name: str, custom_location_namespace: str, - pas_version: str, **kwargs, ): from uuid import uuid4 @@ -276,11 +276,12 @@ def deploy( from azure.core.exceptions import HttpResponseError from azure.identity import DefaultAzureCredential from azure.mgmt.resource import ResourceManagementClient - from rich.console import Console + from rich.console import Console, NewLine from rich.progress import Progress, SpinnerColumn, TimeElapsedColumn from rich.table import Table + from rich.live import Live - version_def = process_deployable_version(pas_version, **kwargs) + version_def = process_deployable_version(**kwargs) show_pas_version = kwargs.get("show_pas_version", False) if show_pas_version: console = Console() @@ -354,16 +355,17 @@ def deploy( return manifest_builder.manifest no_progress: bool = kwargs.get("no_progress", False) - block: bool = kwargs.get("block", True) - - with Progress( - SpinnerColumn(), - *Progress.get_default_columns(), - "Elapsed:", - TimeElapsedColumn(), - transient=False, - disable=(no_progress is True) or (block is False), - ) as progress: + no_block: bool = kwargs.get("no_block", False) + + with Live(None, transient=False, refresh_per_second=8) as live: + init_progress = Progress( + SpinnerColumn(), + *Progress.get_default_columns(), + "Elapsed:", + TimeElapsedColumn(), + transient=False, + disable=any([no_block, no_progress]), + ) deployment_name = f"azedge.init.pas.{str(uuid4()).replace('-', '')}" deployment_params = { "properties": { @@ -372,21 +374,39 @@ def deploy( } } + what_if = kwargs.get("what_if") + header = "Deployment: {} in progress..." + if what_if: + header = header.format("[orange3]What-If? analysis[/orange3]") + else: + header = header.format(f"[medium_purple4]{deployment_name}[/medium_purple4]") + + if not any([no_block, no_progress]): + grid = Table.grid(expand=False) + grid.add_column() + + grid.add_row(NewLine(1)) + grid.add_row(header) + grid.add_row(NewLine(1)) + grid.add_row(init_progress) + + live.update(grid) + init_progress.add_task(description=f"PAS version: {version_def.version}", total=None) + try: - if kwargs.get("what_if"): + if what_if: from azure.cli.command_modules.resource.custom import format_what_if_operation_result - progress.add_task(description=f"What-if of deploying PAS version: {version_def.version}", total=None) what_if_deployment = resource_client.deployments.begin_what_if( resource_group_name=resource_group_name, deployment_name=deployment_name, parameters=deployment_params, ).result() - progress.stop() + live.stop() + init_progress.stop() print(format_what_if_operation_result(what_if_operation_result=what_if_deployment)) return - progress.add_task(description=f"Deploying PAS version: {version_def.version}", total=None) deployment = resource_client.deployments.begin_create_or_update( resource_group_name=resource_group_name, deployment_name=deployment_name, @@ -400,7 +420,7 @@ def deploy( "namespace": cluster_namespace, "deploymentState": {"timestampUtc": {"started": get_timestamp_now_utc()}}, } - if not block: + if no_block: result["deploymentState"]["status"] = deployment.status() return result @@ -422,10 +442,10 @@ def deploy( return -def process_deployable_version(aio_version: str, **kwargs) -> PasVersionDef: +def process_deployable_version(**kwargs) -> PasVersionDef: from ...util import assemble_nargs_to_dict - base_version_def = get_pas_version_def(version=aio_version) + base_version_def = get_pas_version_def(version=DEPLOYABLE_PAS_VERSION) custom_version = kwargs.get("custom_version") only_deploy_custom = kwargs.get("only_deploy_custom") diff --git a/azext_edge/edge/providers/orchestration/pas_versions.py b/azext_edge/edge/providers/orchestration/pas_versions.py index 2f302dc6b..c0bf642ec 100644 --- a/azext_edge/edge/providers/orchestration/pas_versions.py +++ b/azext_edge/edge/providers/orchestration/pas_versions.py @@ -120,3 +120,6 @@ def get_pas_version_def(version: str) -> PasVersionDef: EdgeExtensionName.assets.value: "microsoft.deviceregistry.assets", EdgeExtensionName.akri.value: "microsoft.akri", } + + +DEPLOYABLE_PAS_VERSION = DeployablePasVersions.v012.value diff --git a/azext_edge/edge/util/__init__.py b/azext_edge/edge/util/__init__.py index 03ac8e57a..a49acc668 100644 --- a/azext_edge/edge/util/__init__.py +++ b/azext_edge/edge/util/__init__.py @@ -4,6 +4,6 @@ # Private distribution for NDA customers only. Governed by license terms at https://preview.e4k.dev/docs/use-terms/ # -------------------------------------------------------------------------------------------- -from .common import assemble_nargs_to_dict, scantree, get_timestamp_now_utc +from .common import assemble_nargs_to_dict, scantree, get_timestamp_now_utc, set_log_level -__all__ = ["assemble_nargs_to_dict", "scantree", "get_timestamp_now_utc"] +__all__ = ["assemble_nargs_to_dict", "scantree", "get_timestamp_now_utc", "set_log_level"] diff --git a/azext_edge/edge/util/common.py b/azext_edge/edge/util/common.py index 5d967d527..28c54bf9b 100644 --- a/azext_edge/edge/util/common.py +++ b/azext_edge/edge/util/common.py @@ -5,11 +5,12 @@ # -------------------------------------------------------------------------------------------- """ -utility: Defines common utility functions and components. +common: Defines common utility functions and components. """ import os +import logging from typing import List, Dict from knack.log import get_logger @@ -52,3 +53,8 @@ def get_timestamp_now_utc(format: str = "%Y-%m-%dT%H:%M:%S") -> str: timestamp = datetime.now(timezone.utc).strftime(format) return timestamp + + +def set_log_level(log_name: str, log_level: int = logging.DEBUG): + lgr = logging.getLogger(log_name) + lgr.setLevel(log_level) diff --git a/azext_edge/tests/edge/init/test_init_unit.py b/azext_edge/tests/edge/init/test_init_unit.py index 0230c5ad3..6b8649a66 100644 --- a/azext_edge/tests/edge/init/test_init_unit.py +++ b/azext_edge/tests/edge/init/test_init_unit.py @@ -4,23 +4,27 @@ # Private distribution for NDA customers only. Governed by license terms at https://preview.e4k.dev/docs/use-terms/ # -------------------------------------------------------------------------------------------- -from functools import partial from typing import Dict, Optional import pytest from azext_edge.edge.commands_edge import init -from azext_edge.edge.common import DeployablePasVersions -from azext_edge.edge.providers.orchestration import EdgeServiceMoniker, extension_name_to_type_map, get_pas_version_def +from azext_edge.edge.providers.orchestration import ( + EdgeServiceMoniker, + extension_name_to_type_map, + get_pas_version_def, + DEPLOYABLE_PAS_VERSION, + moniker_to_extension_type_map, +) from azext_edge.edge.util import assemble_nargs_to_dict from ...generators import generate_generic_id @pytest.mark.parametrize( - "cluster_name,cluster_namespace,rg,custom_location_name,custom_location_namespace,location,pas_version," + "cluster_name,cluster_namespace,rg,custom_location_name,custom_location_namespace,location," "processor_instance_name,simulate_plc,opcua_discovery_endpoint,create_sync_rules," - "custom_version,target_name", + "custom_version,only_deploy_custom,target_name", [ pytest.param( generate_generic_id(), # cluster_name @@ -29,12 +33,12 @@ generate_generic_id(), # custom_location_name generate_generic_id(), # custom_location_namespace generate_generic_id(), # location - DeployablePasVersions.v012.value, generate_generic_id(), # processor_instance_name False, # simulate_plc generate_generic_id(), # opcua_discovery_endpoint True, # create_sync_rules None, # custom_version + False, # only_deploy_custom generate_generic_id(), # target_name ), pytest.param( @@ -44,12 +48,27 @@ None, # custom_location_name None, # custom_location_namespace None, # location - DeployablePasVersions.v012.value, None, # processor_instance_name True, # simulate_plc None, # opcua_discovery_endpoint False, # create_sync_rules ["e4k=1.0.0", "symphony=1.2.3"], # custom_version + False, # only_deploy_custom + None, # target_name + ), + pytest.param( + generate_generic_id(), # cluster_name + "default", # cluster_namespace + generate_generic_id(), # rg + None, # custom_location_name + None, # custom_location_namespace + None, # location + None, # processor_instance_name + True, # simulate_plc + None, # opcua_discovery_endpoint + False, # create_sync_rules + ["e4k=1.0.0", "opcua=3.2.1"], # custom_version + True, # only_deploy_custom None, # target_name ), ], @@ -63,16 +82,15 @@ def test_init_show_template( custom_location_name, custom_location_namespace, location, - pas_version, processor_instance_name, simulate_plc, opcua_discovery_endpoint, create_sync_rules, custom_version, + only_deploy_custom, target_name, ): - partial_init = partial( - init, + template = init( cmd=mocked_cmd, cluster_name=cluster_name, cluster_namespace=cluster_namespace, @@ -85,17 +103,15 @@ def test_init_show_template( opcua_discovery_endpoint=opcua_discovery_endpoint, create_sync_rules=create_sync_rules, custom_version=custom_version, + only_deploy_custom=only_deploy_custom, target_name=target_name, - ) - - template = partial_init( show_template=True, - pas_version=pas_version, ) + assert template["$schema"] == "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#" assert template["metadata"]["description"] == "Az Edge CLI PAS deployment." # TODO template versioning. Think about custom. - assert template["contentVersion"] == f"{pas_version}.0" + assert template["contentVersion"] == f"{DEPLOYABLE_PAS_VERSION}.0" assert_template_variables( variables=template["variables"], @@ -110,12 +126,13 @@ def test_init_show_template( cluster_namespace=cluster_namespace, custom_location_name=custom_location_name, custom_location_namespace=custom_location_namespace, - pas_version=pas_version, + pas_version=DEPLOYABLE_PAS_VERSION, processor_instance_name=processor_instance_name, simulate_plc=simulate_plc, opcua_discovery_endpoint=opcua_discovery_endpoint, create_sync_rules=create_sync_rules, custom_version=custom_version, + only_deploy_custom=only_deploy_custom, target_name=target_name, ) @@ -147,6 +164,7 @@ def assert_resources( opcua_discovery_endpoint: Optional[str] = None, create_sync_rules: Optional[str] = None, custom_version: Optional[str] = None, + only_deploy_custom: bool = False, target_name: Optional[str] = None, ): if not custom_version: @@ -160,7 +178,7 @@ def assert_resources( cluster_extension_ids = [] version_def = get_pas_version_def(version=pas_version) if custom_version: - version_def.set_moniker_to_version_map(moniker_map=custom_version) + version_def.set_moniker_to_version_map(moniker_map=custom_version, refresh_mappings=only_deploy_custom) deploy_extension_types = {} for ext_name in k8s_extensions: @@ -197,22 +215,32 @@ def assert_resources( ) bluefin_instances = find_resource_type(resources=resources, resource_type="Microsoft.Bluefin/instances") - assert len(bluefin_instances) == 1 - assert_bluefin_instance( - instance=next(iter(bluefin_instances.values())), cluster_name=cluster_name, name=processor_instance_name - ) + if moniker_to_extension_type_map[EdgeServiceMoniker.bluefin.value] in version_def.extension_to_vers_map: + assert len(bluefin_instances) == 1 + assert_bluefin_instance( + instance=next(iter(bluefin_instances.values())), cluster_name=cluster_name, name=processor_instance_name + ) + else: + assert len(bluefin_instances) == 0 symphony_targets = find_resource_type(resources=resources, resource_type="Microsoft.Symphony/targets") - assert len(symphony_targets) == 1 - assert_symphony_target( - target=next(iter(symphony_targets.values())), - name=target_name, - cluster_name=cluster_name, - namespace=cluster_namespace, - versions=version_def.moniker_to_version_map, - simulate_plc=simulate_plc, - opcua_discovery_endpoint=opcua_discovery_endpoint, - ) + if ( + version_def.moniker_to_version_map.get(EdgeServiceMoniker.obs.value) + or version_def.moniker_to_version_map.get(EdgeServiceMoniker.akri.value) + or version_def.moniker_to_version_map.get(EdgeServiceMoniker.opcua.value) + ): + assert len(symphony_targets) == 1 + assert_symphony_target( + target=next(iter(symphony_targets.values())), + name=target_name, + cluster_name=cluster_name, + namespace=cluster_namespace, + versions=version_def.moniker_to_version_map, + simulate_plc=simulate_plc, + opcua_discovery_endpoint=opcua_discovery_endpoint, + ) + else: + assert len(symphony_targets) == 0 if create_sync_rules: resource_sync_rules = find_resource_type(