diff --git a/src/aosm/README.md b/src/aosm/README.md new file mode 100644 index 00000000000..61c796677aa --- /dev/null +++ b/src/aosm/README.md @@ -0,0 +1,57 @@ +# Microsoft Azure CLI 'aosm' Extension +========================================== + +This package is for the 'aosm' extension to support Azure Operator Service Manager +functions. +i.e. `az aosm` + +Install via `az extension add --name aosm` + + +# Background +The `az aosm` extension provides support for publishing Network Function Definitions +to use with Azure Operator Service Manager or Network Function Manager. + +# Pre-requisites +## VNFs +For VNFs, you will need a single ARM template which would create the Azure resources +for your VNF, for example a Virtual Machine, disks and NICs. You'll also need a VHD +image that would be used for the VNF Virtual Machine. + +# Command examples + +Get help on command arguments + +`az aosm -h` +`az aosm definition -h` +`az aosm definition build -h` +etc... + +All these commands take a `--definition-type` argument of `vnf`, `cnf` or (coming) `nsd` + +Create an example config file for building a definition + +`az aosm definition generate-config --config-file input.json` + +This will output a file called `input.json` which must be filled in. +Once the config file has been filled in the following commands can be run. + +Build a definition locally + +`az aosm definition build --config-file input.json` + +Build and publish a definition + +`az aosm definition build --config-file input.json --publish` + +Publish a pre-built definition + +`az aosm definition publish --config-file input.json` + +Delete a published definition + +`az aosm definition delete --config-file input.json` + +Delete a published definition and the publisher, artifact stores and NFD group + +`az aosm definition delete --config-file input.json --clean` diff --git a/src/aosm/README.rst b/src/aosm/README.rst deleted file mode 100644 index dca4757fd38..00000000000 --- a/src/aosm/README.rst +++ /dev/null @@ -1,5 +0,0 @@ -Microsoft Azure CLI 'aosm' Extension -========================================== - -This package is for the 'aosm' extension. -i.e. 'az aosm' \ No newline at end of file diff --git a/src/aosm/azext_aosm/__init__.py b/src/aosm/azext_aosm/__init__.py index a9098c4d1fb..c15badcb435 100644 --- a/src/aosm/azext_aosm/__init__.py +++ b/src/aosm/azext_aosm/__init__.py @@ -13,9 +13,7 @@ def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType aosm_custom = CliCommandType(operations_tmpl="azext_aosm.custom#{}") - super(AosmCommandsLoader, self).__init__( - cli_ctx=cli_ctx, custom_command_type=aosm_custom - ) + super().__init__(cli_ctx=cli_ctx, custom_command_type=aosm_custom) def load_command_table(self, args): from azext_aosm.commands import load_command_table diff --git a/src/aosm/azext_aosm/_configuration.py b/src/aosm/azext_aosm/_configuration.py index 0bcd9eaedeb..a44606bdd0f 100644 --- a/src/aosm/azext_aosm/_configuration.py +++ b/src/aosm/azext_aosm/_configuration.py @@ -1,36 +1,53 @@ from dataclasses import dataclass, field -from typing import Optional, Any, List +from typing import Dict, Optional, Any, List +from pathlib import Path from azure.cli.core.azclierror import ValidationError, InvalidArgumentValueError -from ._constants import VNF, CNF, NSD +from azext_aosm.util.constants import VNF_DEFINITION_OUTPUT_BICEP_PREFIX, VNF, CNF, NSD + +DESCRIPTION_MAP: Dict[str, str] = { + "publisher_name": "Name of the Publisher resource you want you definition published to", + "publisher_resource_group_name": "Resource group the Publisher resource is in or you want it to be in", + "nf_name": "Name of NF definition", + "version": "Version of the NF definition", + "acr_artifact_store_name": "Name of the ACR Artifact Store resource", + "location": "Azure location of the resources", + "blob_artifact_store_name": "Name of the storage account Artifact Store resource", + "artifact_name": "Name of the artifact", + "file_path": ( + "Optional. File path of the artifact you wish to upload from your " + "local disk. Delete if not required." + ), + "blob_sas_url": ( + "Optional. SAS URL of the blob artifact you wish to copy to your " + "Artifact Store. Delete if not required." + ), + "artifact_version": ( + "Version of the artifact. For VHDs this must be in format A-B-C. " + "For ARM templates this must be in format A.B.C" + ), +} @dataclass class ArtifactConfig: - artifact_name: str = "Name of the artifact" - file_path: Optional[ - str - ] = "File path of the artifact you wish to upload from your local disk" - blob_sas_url: Optional[ - str - ] = "SAS URL of the blob artifact you wish to copy to your Artifact Store" - version: str = ( - "Version of the artifact. For VHDs this must be in format A-B-C. " - "For ARM templates this must be in format A.B.C" - ) + artifact_name: str = DESCRIPTION_MAP["artifact_name"] + # artifact.py checks for the presence of the default descriptions, change there if + # you change the descriptions. + file_path: Optional[str] = DESCRIPTION_MAP["file_path"] + blob_sas_url: Optional[str] = DESCRIPTION_MAP["blob_sas_url"] + version: str = DESCRIPTION_MAP["artifact_version"] @dataclass class Configuration: - publisher_name: str = ( - "Name of the Publisher resource you want you definition published to" - ) - publisher_resource_group_name: str = ( - "Resource group the Publisher resource is in or you want it to be in" - ) - nf_name: str = "Name of NF definition" - version: str = "Version of the NF definition" - acr_artifact_store_name: str = "Name of the ACR Artifact Store resource" - location: str = "Azure location of the resources" + publisher_name: str = DESCRIPTION_MAP["publisher_name"] + publisher_resource_group_name: str = DESCRIPTION_MAP[ + "publisher_resource_group_name" + ] + nf_name: str = DESCRIPTION_MAP["nf_name"] + version: str = DESCRIPTION_MAP["version"] + acr_artifact_store_name: str = DESCRIPTION_MAP["acr_artifact_store_name"] + location: str = DESCRIPTION_MAP["location"] @property def nfdg_name(self) -> str: @@ -45,23 +62,34 @@ def acr_manifest_name(self) -> str: @dataclass class VNFConfiguration(Configuration): - blob_artifact_store_name: str = ( - "Name of the storage account Artifact Store resource" - ) + blob_artifact_store_name: str = DESCRIPTION_MAP["blob_artifact_store_name"] arm_template: Any = ArtifactConfig() vhd: Any = ArtifactConfig() - + def __post_init__(self): """ Cope with deserializing subclasses from dicts to ArtifactConfig. - + Used when creating VNFConfiguration object from a loaded json config file. """ if isinstance(self.arm_template, dict): self.arm_template = ArtifactConfig(**self.arm_template) - + if isinstance(self.vhd, dict): self.vhd = ArtifactConfig(**self.vhd) + + @property + def sa_manifest_name(self) -> str: + """Return the Storage account manifest name from the NFD name.""" + return f"{self.nf_name}-sa-manifest-{self.version.replace('.', '-')}" + + @property + def build_output_folder_name(self) -> str: + """Return the local folder for generating the bicep template to.""" + arm_template_path = self.arm_template.file_path + return ( + f"{VNF_DEFINITION_OUTPUT_BICEP_PREFIX}{Path(str(arm_template_path)).stem}" + ) @dataclass class HelmPackageConfig: @@ -84,8 +112,9 @@ def __post_init__(self): package = HelmPackageConfig(**dict(package)) -def get_configuration(definition_type, config_as_dict=None) -> Configuration: - +def get_configuration( + definition_type: str, config_as_dict: Optional[Dict[Any, Any]] = None +) -> Configuration: if config_as_dict is None: config_as_dict = {} @@ -119,10 +148,18 @@ def validate_configuration(config: Configuration) -> None: raise ValidationError( "Config validation error. VHD artifact version should be in format A-B-C" ) - if ( - "." not in config.arm_template.version - or "-" in config.arm_template.version - ): + if "." not in config.arm_template.version or "-" in config.arm_template.version: raise ValidationError( "Config validation error. ARM template artifact version should be in format A.B.C" ) + + if not ( + (config.vhd.file_path or config.vhd.blob_sas_url) + or ( + config.vhd.file_path == DESCRIPTION_MAP["file_path"] + and config.vhd.blob_sas_url == DESCRIPTION_MAP["blob_sas_url"] + ) + ): + raise ValidationError( + "Config validation error. VHD config must have either a local filepath or a blob SAS URL" + ) diff --git a/src/aosm/azext_aosm/_help.py b/src/aosm/azext_aosm/_help.py index d5169d49c0d..2a9a3013fd9 100644 --- a/src/aosm/azext_aosm/_help.py +++ b/src/aosm/azext_aosm/_help.py @@ -21,6 +21,13 @@ short-summary: Manage AOSM publisher definitions. """ +helps[ + "aosm definition generate-config" +] = """ + type: command + short-summary: Generate configuration file for building an AOSM publisher definition. +""" + helps[ "aosm definition build" ] = """ @@ -29,12 +36,13 @@ """ helps[ - "aosm definition generate-config" + "aosm definition publish" ] = """ type: command - short-summary: Generate configuration file for building an AOSM publisher definition. + short-summary: Publish a pre-built AOSM publisher definition. """ + helps[ "aosm definition delete" ] = """ diff --git a/src/aosm/azext_aosm/_params.py b/src/aosm/azext_aosm/_params.py index c9339763f4d..9c13619fabf 100644 --- a/src/aosm/azext_aosm/_params.py +++ b/src/aosm/azext_aosm/_params.py @@ -6,8 +6,9 @@ from argcomplete.completers import FilesCompleter from azure.cli.core import AzCommandsLoader -#from knack.arguments import CLIArgumentType -from ._constants import VNF, CNF, NSD + +# from knack.arguments import CLIArgumentType +from .util.constants import VNF, CNF, NSD def load_arguments(self: AzCommandsLoader, _): @@ -42,6 +43,34 @@ def load_arguments(self: AzCommandsLoader, _): arg_type=get_three_state_flag(), help="Also delete artifact stores, NFD Group and Publisher. Use with care.", ) + c.argument( + "definition_file", + options_list=["--definition-file", "-b"], + type=file_type, + completer=FilesCompleter(allowednames="*.json"), + help="Optional path to a bicep file to publish. Use to override publish of the built definition with an alternative file.", + ) + c.argument( + "parameters_json_file", + options_list=["--parameters-file", "-p"], + type=file_type, + completer=FilesCompleter(allowednames="*.json"), + help="Optional path to a parameters file for the bicep definition file. Use to override publish of the built definition and config with alternative parameters.", + ) + c.argument( + "manifest_file", + options_list=["--manifest-file", "-m"], + type=file_type, + completer=FilesCompleter(allowednames="*.json"), + help="Optional path to a bicep file to publish manifests. Use to override publish of the built definition with an alternative file.", + ) + c.argument( + "manifest_parameters_json_file", + options_list=["--manifest-parameters-file", "-mp"], + type=file_type, + completer=FilesCompleter(allowednames="*.json"), + help="Optional path to a parameters file for the manifest definition file. Use to override publish of the built definition and config with alternative parameters.", + ) with self.argument_context("aosm generate-config") as c: c.argument( diff --git a/src/aosm/azext_aosm/commands.py b/src/aosm/azext_aosm/commands.py index d57a4b9db3b..c906ca6947e 100644 --- a/src/aosm/azext_aosm/commands.py +++ b/src/aosm/azext_aosm/commands.py @@ -5,7 +5,8 @@ # pylint: disable=line-too-long from azure.cli.core import AzCommandsLoader -#from azure.cli.core.commands import CliCommandType + +# from azure.cli.core.commands import CliCommandType from azext_aosm._client_factory import cf_aosm @@ -21,6 +22,7 @@ def load_command_table(self: AzCommandsLoader, _): g.custom_command("build", "build_definition") g.custom_command("delete", "delete_published_definition") g.custom_command("show", "show_publisher") + g.custom_command("publish", "publish_definition") with self.command_group("aosm", is_preview=True): pass diff --git a/src/aosm/azext_aosm/custom.py b/src/aosm/azext_aosm/custom.py index 0382c9f59df..b615e67916e 100644 --- a/src/aosm/azext_aosm/custom.py +++ b/src/aosm/azext_aosm/custom.py @@ -5,19 +5,21 @@ import json from dataclasses import asdict +from typing import Optional from knack.log import get_logger from azext_aosm.generate_nfd.cnf_nfd_generator import CnfNfdGenerator from azext_aosm.generate_nfd.nfd_generator_base import NFDGenerator from azext_aosm.generate_nfd.vnf_bicep_nfd_generator import VnfBicepNfdGenerator from azext_aosm.deploy.deploy_with_arm import DeployerViaArm -from azext_aosm._constants import VNF, CNF, NSD -from azext_aosm.util.management_clients import ApiClientsAndCaches -from .vendored_sdks import HybridNetworkManagementClient -from ._client_factory import cf_resources -from ._configuration import ( +from azext_aosm.util.constants import VNF, CNF # , NSD +from azext_aosm.util.management_clients import ApiClients +from azext_aosm.vendored_sdks import HybridNetworkManagementClient +from azext_aosm._client_factory import cf_resources +from azext_aosm._configuration import ( get_configuration, validate_configuration, + Configuration, ) @@ -27,71 +29,76 @@ def build_definition( cmd, client: HybridNetworkManagementClient, - definition_type, - config_file, + definition_type: str, + config_file: str, publish=False, ): - """Build and optionally publish a definition + """ + Build and optionally publish a definition. - :param cmd: _description_ + :param cmd: :type cmd: _type_ - :param client: _description_ + :param client: :type client: HybridNetworkManagementClient - :param definition_type: _description_ - :type definition_type: _type_ - :param config_file: _description_ - :type config_file: _type_ + :param config_file: path to the file + :param definition_type: VNF, CNF or NSD :param publish: _description_, defaults to False :type publish: bool, optional """ - with open(config_file, "r", encoding="utf-8") as f: - config_as_dict = json.loads(f.read()) - - apiClientsAndCaches = ApiClientsAndCaches( + api_clients = ApiClients( aosm_client=client, resource_client=cf_resources(cmd.cli_ctx) ) - # TODO - this isn't deserializing the config properly - any sub-objects are left - # as a dictionary instead of being converted to the object (e.g. ArtifactConfig) - # se we have to reference them as dictionary values - config = get_configuration(definition_type, config_as_dict) - validate_configuration(config) + # Read the config from the given file + config = _get_config_from_file(config_file, definition_type) + # Generate the NFD/NSD and the artifact manifest. _generate_nfd(definition_type=definition_type, config=config) - # Write the ARM/bicep template if that's what we are doing # Publish the definition if publish is true if publish: if definition_type == VNF: - deployer = DeployerViaArm(apiClientsAndCaches, config=config) + deployer = DeployerViaArm(api_clients, config=config) deployer.deploy_vnfd_from_bicep() else: print("TODO - cannot publish CNF or NSD yet.") -def generate_definition_config(definition_type, output_file="input.json"): +def generate_definition_config(definition_type: str, output_file: str = "input.json"): + """ + Generate an example config file for building a definition. + + :param definition_type: CNF, VNF or NSD + :param output_file: path to output config file, defaults to "input.json" + :type output_file: str, optional + """ config = get_configuration(definition_type) config_as_dict = json.dumps(asdict(config), indent=4) with open(output_file, "w", encoding="utf-8") as f: f.write(config_as_dict) - print( - "Empty definition configuration has been written to %s", - output_file, - ) - logger.info( - "Empty definition configuration has been written to %s", - output_file, - ) + print(f"Empty definition configuration has been written to {output_file}") + logger.info(f"Empty definition configuration has been written to {output_file}") -def _generate_nfd(definition_type, config): +def _get_config_from_file(config_file: str, definition_type: str) -> Configuration: """ - _summary_ + Read input config file JSON and turn it into a Configuration object. - :param definition_type: _description_ - :type definition_type: _type_ + :param config_file: path to the file + :param definition_type: VNF, CNF or NSD + :rtype: Configuration """ + with open(config_file, "r", encoding="utf-8") as f: + config_as_dict = json.loads(f.read()) + + config = get_configuration(definition_type, config_as_dict) + validate_configuration(config) + return config + + +def _generate_nfd(definition_type, config): + """Generate a Network Function Definition for the given type and config.""" nfd_generator: NFDGenerator if definition_type == VNF: nfd_generator = VnfBicepNfdGenerator(config) @@ -107,6 +114,51 @@ def _generate_nfd(definition_type, config): nfd_generator.generate_nfd() +def publish_definition( + cmd, + client: HybridNetworkManagementClient, + definition_type, + config_file, + definition_file: Optional[str] = None, + parameters_json_file: Optional[str] = None, + manifest_file: Optional[str] = None, + manifest_parameters_json_file: Optional[str] = None, +): + """ + Publish a generated definition. + + :param cmd: + :param client: + :type client: HybridNetworkManagementClient + :param definition_type: VNF or CNF + :param config_file: Path to the config file for the NFDV + :param definition_file: Optional path to a bicep template to deploy, in case the user + wants to edit the built NFDV template. If omitted, the default + built NFDV template will be used. + :param parameters_json_file: Optional path to a parameters file for the bicep file, + in case the user wants to edit the built NFDV template. If + omitted, parameters from config will be turned into parameters + for the bicep file + :param manifest_file: Optional path to an override bicep template to deploy + manifests + :param manifest_parameters_json_file: Optional path to an override bicep parameters + file for manifest parameters + """ + print("Publishing definition.") + api_clients = ApiClients( + aosm_client=client, resource_client=cf_resources(cmd.cli_ctx) + ) + config = _get_config_from_file(config_file, definition_type) + if definition_type == VNF: + deployer = DeployerViaArm(api_clients, config=config) + deployer.deploy_vnfd_from_bicep( + bicep_path=definition_file, + parameters_json_file=parameters_json_file, + manifest_bicep_path=manifest_file, + manifest_parameters_json_file=manifest_parameters_json_file, + ) + + def delete_published_definition( cmd, client: HybridNetworkManagementClient, @@ -114,12 +166,18 @@ def delete_published_definition( config_file, clean=False, ): - with open(config_file, "r", encoding="utf-8") as f: - config_as_dict = json.loads(f.read()) - config = get_configuration(definition_type, config_as_dict) - validate_configuration(config) + """ + Delete a published definition. + + :param definition_type: CNF or VNF + :param config_file: Path to the config file + :param clean: if True, will delete the NFDG, artifact stores and publisher too. + Defaults to False. Only works if no resources have those as a parent. + Use with care. + """ + config = _get_config_from_file(config_file, definition_type) - api_clients = ApiClientsAndCaches( + api_clients = ApiClients( aosm_client=client, resource_client=cf_resources(cmd.cli_ctx) ) from azext_aosm.delete.delete import ResourceDeleter diff --git a/src/aosm/azext_aosm/delete/delete.py b/src/aosm/azext_aosm/delete/delete.py index 45ce6e3887c..05a9904d7db 100644 --- a/src/aosm/azext_aosm/delete/delete.py +++ b/src/aosm/azext_aosm/delete/delete.py @@ -11,7 +11,7 @@ Provider, ) -from azext_aosm.util.management_clients import ApiClientsAndCaches +from azext_aosm.util.management_clients import ApiClients from azext_aosm._configuration import Configuration, VNFConfiguration from azext_aosm.util.utils import input_ack @@ -22,7 +22,7 @@ class ResourceDeleter: def __init__( self, - apiClientsAndCaches: ApiClientsAndCaches, + ApiClients: ApiClients, config: Configuration, ) -> None: """ @@ -34,7 +34,7 @@ def __init__( :type resource_client: ResourceManagementClient """ logger.debug("Create ARM/Bicep Deployer") - self.api_clients = apiClientsAndCaches + self.api_clients = ApiClients self.config = config def delete_vnf(self, all: bool = False): @@ -152,7 +152,7 @@ def delete_nfdg(self) -> None: network_function_definition_group_name=self.config.nfdg_name, ) poller.result() - print("Delete NFD Group") + print("Deleted NFD Group") except Exception: logger.error(f"Failed to delete NFDG.") raise diff --git a/src/aosm/azext_aosm/deploy/artifact.py b/src/aosm/azext_aosm/deploy/artifact.py index 057a6f409d1..b8b0287b117 100644 --- a/src/aosm/azext_aosm/deploy/artifact.py +++ b/src/aosm/azext_aosm/deploy/artifact.py @@ -7,7 +7,7 @@ from typing import Union from azure.storage.blob import BlobClient -from azext_aosm._configuration import ArtifactConfig +from azext_aosm._configuration import ArtifactConfig, DESCRIPTION_MAP from oras.client import OrasClient logger = get_logger(__name__) @@ -41,11 +41,16 @@ def _upload_to_acr(self, artifact_config: ArtifactConfig) -> None: """ assert type(self.artifact_client) == OrasClient - if "file_path" in artifact_config.keys(): + # If not included in config, the file path value will be the description of + # the field. + if ( + artifact_config.file_path + and not artifact_config.file_path == DESCRIPTION_MAP["file_path"] + ): target = f"{self.artifact_client.remote.hostname.replace('https://', '')}/{self.artifact_name}:{self.artifact_version}" - logger.debug(f"Uploading {artifact_config['file_path']} to {target}") + logger.debug(f"Uploading {artifact_config.file_path} to {target}") self.artifact_client.push( - file=artifact_config["file_path"], + file=artifact_config.file_path, target=target, ) else: @@ -62,14 +67,19 @@ def _upload_to_storage_account(self, artifact_config: ArtifactConfig) -> None: assert type(self.artifact_client) == BlobClient # If the file path is given, upload the artifact, else, copy it from an existing blob. - if "file_path" in artifact_config.keys(): - with open(artifact_config["file_path"], "rb") as artifact: + if ( + artifact_config.file_path + and not artifact_config.file_path == DESCRIPTION_MAP["file_path"] + ): + logger.info("Upload to blob store") + with open(artifact_config.file_path, "rb") as artifact: self.artifact_client.upload_blob(artifact, overwrite=True) logger.info( - f"Successfully uploaded {artifact_config['file_path']} to {self.artifact_client.account_name}" + f"Successfully uploaded {artifact_config.file_path} to {self.artifact_client.account_name}" ) else: - source_blob = BlobClient.from_blob_url(artifact_config["blob_sas_url"]) + logger.info("Copy from SAS URL to blob store") + source_blob = BlobClient.from_blob_url(artifact_config.blob_sas_url) if source_blob.exists(): logger.debug(source_blob.url) diff --git a/src/aosm/azext_aosm/deploy/artifact_manifest.py b/src/aosm/azext_aosm/deploy/artifact_manifest.py index 42a655be533..0d460a717c0 100644 --- a/src/aosm/azext_aosm/deploy/artifact_manifest.py +++ b/src/aosm/azext_aosm/deploy/artifact_manifest.py @@ -13,7 +13,7 @@ from azext_aosm._configuration import Configuration, VNFConfiguration from azext_aosm.vendored_sdks.models import ArtifactAccessCredential, ArtifactManifest -from azext_aosm.util.management_clients import ApiClientsAndCaches +from azext_aosm.util.management_clients import ApiClients logger = get_logger(__name__) @@ -24,7 +24,7 @@ class ArtifactManifestOperator: def __init__( self, config: Configuration, - api_clients: ApiClientsAndCaches, + api_clients: ApiClients, store_name: str, manifest_name: str, ) -> None: @@ -49,7 +49,7 @@ def _manifest_credentials(self) -> Any: def _oras_client(self, acr_url: str) -> OrasClient: """ - Returns an OrasClient object for uploading to the artifact str Returns an OrasClient object for uploading to the artifact store ACR.oe ACR. + Returns an OrasClient object for uploading to the artifact store ACR. :param arc_url: URL of the ACR backing the artifact manifest """ diff --git a/src/aosm/azext_aosm/deploy/deploy_with_arm.py b/src/aosm/azext_aosm/deploy/deploy_with_arm.py index 08b5a723d07..753cb4864bf 100644 --- a/src/aosm/azext_aosm/deploy/deploy_with_arm.py +++ b/src/aosm/azext_aosm/deploy/deploy_with_arm.py @@ -7,19 +7,19 @@ import os import shutil import subprocess # noqa -from typing import Any, Dict +from typing import Any, Dict, Optional from knack.log import get_logger from azext_aosm.deploy.artifact_manifest import ArtifactManifestOperator -from azext_aosm.util.management_clients import ApiClientsAndCaches +from azext_aosm.util.management_clients import ApiClients from azure.mgmt.resource.resources.v2021_04_01.models import DeploymentExtended from pathlib import Path from azext_aosm.deploy.pre_deploy import PreDeployerViaSDK from azext_aosm._configuration import Configuration, VNFConfiguration -from azext_aosm._constants import ( - VNF_DEFINITION_OUTPUT_BICEP_PREFIX, +from azext_aosm.util.constants import ( VNF_DEFINITION_BICEP_SOURCE_TEMPLATE, + VNF_MANIFEST_BICEP_SOURCE_TEMPLATE, ) @@ -33,7 +33,7 @@ class DeployerViaArm: # using the SDK def __init__( self, - apiClientsAndCaches: ApiClientsAndCaches, + api_clients: ApiClients, config: Configuration, ) -> None: """ @@ -45,40 +45,86 @@ def __init__( :type resource_client: ResourceManagementClient """ logger.debug("Create ARM/Bicep Deployer") - self.api_clients = apiClientsAndCaches + self.api_clients = api_clients self.config = config - self.pre_deployer = PreDeployerViaSDK(apiClientsAndCaches, self.config) + self.pre_deployer = PreDeployerViaSDK(api_clients, self.config) - def deploy_vnfd_from_bicep(self) -> None: + def deploy_vnfd_from_bicep( + self, + bicep_path: Optional[str] = None, + parameters_json_file: Optional[str] = None, + manifest_bicep_path: Optional[str] = None, + manifest_parameters_json_file: Optional[str] = None, + ) -> None: """ Deploy the bicep template defining the VNFD. Also ensure that all required predeploy resources are deployed. - :param bicep_template_path: The path to the bicep template of the + :param bicep_template_path: The path to the bicep template of the nfdv :type bicep_template_path: str + :parameters_json_file: path to an override file of set parameters for the nfdv + :param manifest_bicep_path: The path to the bicep template of the manifest + :manifest_parameters_json_file: path to an override file of set parameters for + the manifest """ assert isinstance(self.config, VNFConfiguration) - # TODO - duplicated from vnf_bicep_nfd_generator and won't work if file exists - arm_template_path = self.config.arm_template.file_path - folder_name = ( - f"{VNF_DEFINITION_OUTPUT_BICEP_PREFIX}{Path(str(arm_template_path)).stem}" - ) - bicep_template_name = VNF_DEFINITION_BICEP_SOURCE_TEMPLATE - bicep_path = os.path.join(folder_name, bicep_template_name) + if not bicep_path: + # User has not passed in a bicep template, so we are deploying the default + # one produced from building the NFDV using this CLI + bicep_path = os.path.join( + self.config.build_output_folder_name, + VNF_DEFINITION_BICEP_SOURCE_TEMPLATE, + ) + + if parameters_json_file: + message: str = f"Use parameters from file {parameters_json_file}" + logger.info(message) + print(message) + with open(parameters_json_file, "r", encoding="utf-8") as f: + parameters = json.loads(f.read()) + + else: + # User has not passed in parameters file, so we use the parameters required + # from config for the default bicep template produced from building the + # NFDV using this CLI + logger.debug("Create parameters for default NFDV template.") + parameters = self.construct_vnfd_parameters() - parameters = self.construct_vnfd_parameters() logger.debug(parameters) # Create or check required resources - self.vnfd_predeploy() - self.deploy_bicep_template(bicep_path, parameters) - print( - f"Deployed NFD {self.config.nf_name} version {self.config.version} " + deploy_manifest_template = not self.vnfd_predeploy() + if deploy_manifest_template: + print(f"Deploy bicep template for Artifact manifests") + logger.debug("Deploy manifest bicep") + if not manifest_bicep_path: + manifest_bicep_path = os.path.join( + self.config.build_output_folder_name, + VNF_MANIFEST_BICEP_SOURCE_TEMPLATE, + ) + if not manifest_parameters_json_file: + manifest_params = self.construct_manifest_parameters() + else: + logger.info("Use provided manifest parameters") + with open(manifest_parameters_json_file, "r", encoding="utf-8") as f: + manifest_params = json.loads(f.read()) + self.deploy_bicep_template(manifest_bicep_path, manifest_params) + else: + print( + f"Artifact manifests exist for NFD {self.config.nf_name} " + f"version {self.config.version}" + ) + message = ( + f"Deploy bicep template for NFD {self.config.nf_name} version {self.config.version} " f"into {self.config.publisher_resource_group_name} under publisher " f"{self.config.publisher_name}" ) + print(message) + logger.info(message) + self.deploy_bicep_template(bicep_path, parameters) + print(f"Deployed NFD {self.config.nf_name} version {self.config.version}.") storage_account_manifest = ArtifactManifestOperator( self.config, @@ -102,11 +148,11 @@ def deploy_vnfd_from_bicep(self) -> None: arm_template_artifact.upload(self.config.arm_template) print("Done") - def vnfd_predeploy(self): + def vnfd_predeploy(self) -> bool: """ All the predeploy steps for a VNF. Create publisher, artifact stores and NFDG. - VNF specific + VNF specific return True if artifact manifest already exists, False otherwise """ logger.debug("Ensure all required resources exist") self.pre_deployer.ensure_config_resource_group_exists() @@ -114,6 +160,7 @@ def vnfd_predeploy(self): self.pre_deployer.ensure_acr_artifact_store_exists() self.pre_deployer.ensure_sa_artifact_store_exists() self.pre_deployer.ensure_config_nfdg_exists() + return self.pre_deployer.do_config_artifact_manifests_exist() def construct_vnfd_parameters(self) -> Dict[str, Any]: """ @@ -126,11 +173,26 @@ def construct_vnfd_parameters(self) -> Dict[str, Any]: "publisherName": {"value": self.config.publisher_name}, "acrArtifactStoreName": {"value": self.config.acr_artifact_store_name}, "saArtifactStoreName": {"value": self.config.blob_artifact_store_name}, - "acrManifesteName": {"value": self.config.acr_manifest_name}, - "saManifesteName": {"value": self.config.sa_manifest_name}, "nfName": {"value": self.config.nf_name}, "nfDefinitionGroup": {"value": self.config.nfdg_name}, "nfDefinitionVersion": {"value": self.config.version}, + "vhdVersion": {"value": self.config.vhd.version}, + "armTemplateVersion": {"value": self.config.arm_template.version}, + } + + def construct_manifest_parameters(self) -> Dict[str, Any]: + """ + Create the parmeters dictionary for vnfdefinitions.bicep. VNF specific. + + :param config: The contents of the configuration file. + """ + assert isinstance(self.config, VNFConfiguration) + return { + "publisherName": {"value": self.config.publisher_name}, + "acrArtifactStoreName": {"value": self.config.acr_artifact_store_name}, + "saArtifactStoreName": {"value": self.config.blob_artifact_store_name}, + "acrManifestName": {"value": self.config.acr_manifest_name}, + "saManifestName": {"value": self.config.sa_manifest_name}, "vhdName": {"value": self.config.vhd.artifact_name}, "vhdVersion": {"value": self.config.vhd.version}, "armTemplateName": {"value": self.config.arm_template.artifact_name}, @@ -236,6 +298,7 @@ def validate_and_deploy_arm_template( # Wait for the deployment to complete and get the outputs deployment: DeploymentExtended = poller.result() + logger.debug("Finished deploying") if deployment.properties is not None: depl_props = deployment.properties diff --git a/src/aosm/azext_aosm/deploy/pre_deploy.py b/src/aosm/azext_aosm/deploy/pre_deploy.py index a4e332f5b34..320139efadf 100644 --- a/src/aosm/azext_aosm/deploy/pre_deploy.py +++ b/src/aosm/azext_aosm/deploy/pre_deploy.py @@ -10,9 +10,7 @@ from azure.cli.core.azclierror import AzCLIError from azure.mgmt.resource.resources.v2022_09_01.models import ResourceGroup -from azure.mgmt.resource import ResourceManagementClient -from azext_aosm.util.management_clients import ApiClientsAndCaches -from azext_aosm.vendored_sdks import HybridNetworkManagementClient +from azext_aosm.util.management_clients import ApiClients from azext_aosm.vendored_sdks.models import ( ArtifactStore, ArtifactStoreType, @@ -21,7 +19,7 @@ Publisher, ) from azext_aosm._configuration import Configuration, VNFConfiguration -from azext_aosm._constants import PROV_STATE_SUCCEEDED +from azext_aosm.util.constants import PROV_STATE_SUCCEEDED logger = get_logger(__name__) @@ -31,7 +29,7 @@ class PreDeployerViaSDK: def __init__( self, - apiClientsAndCaches: ApiClientsAndCaches, + api_clients: ApiClients, config: Configuration, ) -> None: """ @@ -43,7 +41,7 @@ def __init__( :type resource_client: ResourceManagementClient """ - self.api_clients = apiClientsAndCaches + self.api_clients = api_clients self.config = config def ensure_resource_group_exists(self, resource_group_name: str) -> None: @@ -270,6 +268,51 @@ def ensure_config_nfdg_exists( self.config.location, ) + def does_artifact_manifest_exist( + self, rg_name: str, publisher_name: str, store_name: str, manifest_name: str + ) -> bool: + try: + self.api_clients.aosm_client.artifact_manifests.get( + resource_group_name=rg_name, + publisher_name=publisher_name, + artifact_store_name=store_name, + artifact_manifest_name=manifest_name, + ) + logger.debug(f"Artifact manifest {manifest_name} exists") + return True + except azure_exceptions.ResourceNotFoundError: + logger.debug(f"Artifact manifest {manifest_name} does not exist") + return False + + def do_config_artifact_manifests_exist( + self, + ): + """Returns True if all required manifests exist, False otherwise.""" + acr_manny_exists: bool = self.does_artifact_manifest_exist( + rg_name=self.config.publisher_resource_group_name, + publisher_name=self.config.publisher_name, + store_name=self.config.acr_artifact_store_name, + manifest_name=self.config.acr_manifest_name, + ) + + if isinstance(self.config, VNFConfiguration): + sa_manny_exists: bool = self.does_artifact_manifest_exist( + rg_name=self.config.publisher_resource_group_name, + publisher_name=self.config.publisher_name, + store_name=self.config.blob_artifact_store_name, + manifest_name=self.config.sa_manifest_name, + ) + if acr_manny_exists and sa_manny_exists: + return True + elif acr_manny_exists or sa_manny_exists: + raise AzCLIError( + "Only one artifact manifest exists. Cannot proceed. Please delete the NFDV using `az aosm definition delete` and start the publish again from scratch." + ) + else: + return False + + return acr_manny_exists + def ensure_nsdg_exists( self, resource_group_name: str, diff --git a/src/aosm/azext_aosm/generate_nfd/nfd_generator_base.py b/src/aosm/azext_aosm/generate_nfd/nfd_generator_base.py index 2408972bc34..81afb4db802 100644 --- a/src/aosm/azext_aosm/generate_nfd/nfd_generator_base.py +++ b/src/aosm/azext_aosm/generate_nfd/nfd_generator_base.py @@ -4,7 +4,6 @@ # -------------------------------------------------------------------------------------- """Contains a base class for generating NFDs.""" from knack.log import get_logger -from azext_aosm._configuration import Configuration logger = get_logger(__name__) @@ -15,17 +14,13 @@ class NFDGenerator: def __init__( self, - # config: Configuration ) -> None: """ - _summary_ + Superclass for NFD generators. - :param definition_type: _description_ - :type definition_type: str - :param config: _description_ - :type config: Configuration + The sub-classes do the actual work """ - # self.config = config + pass def generate_nfd(self) -> None: """No-op on base class.""" diff --git a/src/aosm/azext_aosm/generate_nfd/templates/publisher_definition.bicep b/src/aosm/azext_aosm/generate_nfd/templates/publisher_definition.bicep deleted file mode 100644 index 62fd4aef354..00000000000 --- a/src/aosm/azext_aosm/generate_nfd/templates/publisher_definition.bicep +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Highly Confidential Material -// Bicep template to create a Publisher -param location string = resourceGroup().location -@description('Name you want to give the new Publisher object') -param publisherName string - -resource publisher 'Microsoft.HybridNetwork/publishers@2022-09-01-preview' = { - name: publisherName - scope: resourceGroup() - location: location - properties: { - scope: 'Private' - } -} diff --git a/src/aosm/azext_aosm/generate_nfd/templates/vnfartifactmanifests.bicep b/src/aosm/azext_aosm/generate_nfd/templates/vnfartifactmanifests.bicep new file mode 100644 index 00000000000..20e7d5e2e2b --- /dev/null +++ b/src/aosm/azext_aosm/generate_nfd/templates/vnfartifactmanifests.bicep @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. + +// This file creates an NF definition for a VNF +param location string = resourceGroup().location +@description('Name of an existing publisher, expected to be in the resource group where you deploy the template') +param publisherName string +@description('Name of an existing ACR-backed Artifact Store, deployed under the publisher.') +param acrArtifactStoreName string +@description('Name of an existing Storage Account-backed Artifact Store, deployed under the publisher.') +param saArtifactStoreName string +@description('Name of the manifest to deploy for the ACR-backed Artifact Store') +param acrManifestName string +@description('Name of the manifest to deploy for the Storage Account-backed Artifact Store') +param saManifestName string +@description('The name under which to store the VHD') +param vhdName string +@description('The version that you want to name the NFM VHD artifact, in format A-B-C. e.g. 6-13-0') +param vhdVersion string +@description('The name under which to store the ARM template') +param armTemplateName string +@description('The version that you want to name the NFM template artifact, in format A.B.C. e.g. 6.13.0. If testing for development, you can use any numbers you like.') +param armTemplateVersion string + +// Created by the az aosm definition publish command before the template is deployed +resource publisher 'Microsoft.HybridNetwork/publishers@2022-09-01-preview' existing = { + name: publisherName + scope: resourceGroup() +} + +// Created by the az aosm definition publish command before the template is deployed +resource acrArtifactStore 'Microsoft.HybridNetwork/publishers/artifactStores@2022-09-01-preview' existing = { + parent: publisher + name: acrArtifactStoreName +} + +// Created by the az aosm definition publish command before the template is deployed +resource saArtifactStore 'Microsoft.HybridNetwork/publishers/artifactStores@2022-09-01-preview' existing = { + parent: publisher + name: saArtifactStoreName +} + +resource saArtifactManifest 'Microsoft.Hybridnetwork/publishers/artifactStores/artifactManifests@2022-09-01-preview' = { + parent: saArtifactStore + name: saManifestName + location: location + properties: { + artifacts: [ + { + artifactName: '${vhdName}' + artifactType: 'VhdImageFile' + artifactVersion: vhdVersion + } + ] + } +} + +resource acrArtifactManifest 'Microsoft.Hybridnetwork/publishers/artifactStores/artifactManifests@2022-09-01-preview' = { + parent: acrArtifactStore + name: acrManifestName + location: location + properties: { + artifacts: [ + { + artifactName: '${armTemplateName}' + artifactType: 'ArmTemplate' + artifactVersion: armTemplateVersion + } + ] + } +} diff --git a/src/aosm/azext_aosm/generate_nfd/templates/vnfdefinition.bicep b/src/aosm/azext_aosm/generate_nfd/templates/vnfdefinition.bicep index aafdd474de5..87f3b93e15f 100644 --- a/src/aosm/azext_aosm/generate_nfd/templates/vnfdefinition.bicep +++ b/src/aosm/azext_aosm/generate_nfd/templates/vnfdefinition.bicep @@ -8,22 +8,14 @@ param publisherName string param acrArtifactStoreName string @description('Name of an existing Storage Account-backed Artifact Store, deployed under the publisher.') param saArtifactStoreName string -@description('Name of the manifest to deploy for the ACR-backed Artifact Store') -param acrManifestName string -@description('Name of the manifest to deploy for the Storage Account-backed Artifact Store') -param saManifestName string @description('Name of Network Function. Used predominantly as a prefix for other variable names') param nfName string @description('Name of an existing Network Function Definition Group') param nfDefinitionGroup string @description('The version of the NFDV you want to deploy, in format A-B-C') param nfDefinitionVersion string -@description('The name under which to store the VHD') -param vhdName string @description('The version that you want to name the NFM VHD artifact, in format A-B-C. e.g. 6-13-0') param vhdVersion string -@description('The name under which to store the ARM template') -param armTemplateName string @description('The version that you want to name the NFM template artifact, in format A.B.C. e.g. 6.13.0. If testing for development, you can use any numbers you like.') param armTemplateVersion string @@ -51,36 +43,6 @@ resource nfdg 'Microsoft.Hybridnetwork/publishers/networkfunctiondefinitiongroup name: nfDefinitionGroup } -resource saArtifactManifest 'Microsoft.Hybridnetwork/publishers/artifactStores/artifactManifests@2022-09-01-preview' = { - parent: saArtifactStore - name: saManifestName - location: location - properties: { - artifacts: [ - { - artifactName: '${vhdName}' - artifactType: 'VhdImageFile' - artifactVersion: vhdVersion - } - ] - } -} - -resource acrArtifactManifest 'Microsoft.Hybridnetwork/publishers/artifactStores/artifactManifests@2022-09-01-preview' = { - parent: acrArtifactStore - name: acrManifestName - location: location - properties: { - artifacts: [ - { - artifactName: '${armTemplateName}' - artifactType: 'ArmTemplate' - artifactVersion: armTemplateVersion - } - ] - } -} - resource nfdv 'Microsoft.Hybridnetwork/publishers/networkfunctiondefinitiongroups/networkfunctiondefinitionversions@2022-09-01-preview' = { parent: nfdg name: nfDefinitionVersion diff --git a/src/aosm/azext_aosm/generate_nfd/vnf_bicep_nfd_generator.py b/src/aosm/azext_aosm/generate_nfd/vnf_bicep_nfd_generator.py index 5547cd1aa16..2b0768c6291 100644 --- a/src/aosm/azext_aosm/generate_nfd/vnf_bicep_nfd_generator.py +++ b/src/aosm/azext_aosm/generate_nfd/vnf_bicep_nfd_generator.py @@ -15,12 +15,9 @@ from azext_aosm.generate_nfd.nfd_generator_base import NFDGenerator from azext_aosm._configuration import VNFConfiguration -from azext_aosm.publisher_resources.publisher_resources import ( - PublisherResourceGenerator, -) -from azext_aosm._constants import ( +from azext_aosm.util.constants import ( VNF_DEFINITION_BICEP_SOURCE_TEMPLATE, - VNF_DEFINITION_OUTPUT_BICEP_PREFIX, + VNF_MANIFEST_BICEP_SOURCE_TEMPLATE, ) @@ -29,27 +26,31 @@ class VnfBicepNfdGenerator(NFDGenerator): """ - _summary_ + VNF NFD Generator. - :param NFDGenerator: _description_ - :type NFDGenerator: _type_ + This takes a source ARM template and a config file, and outputs: + - A bicep file for the NFDV + - Parameters files that are used by the NFDV bicep file, these are the + deployParameters and the mapping profiles of those deploy parameters + - A bicep file for the Artifact manifests """ def __init__(self, config: VNFConfiguration): - super(NFDGenerator, self).__init__( - # config=config, - ) + super(NFDGenerator, self).__init__() self.config = config self.bicep_template_name = VNF_DEFINITION_BICEP_SOURCE_TEMPLATE + self.manifest_template_name = VNF_MANIFEST_BICEP_SOURCE_TEMPLATE self.arm_template_path = self.config.arm_template.file_path - self.folder_name = f"{VNF_DEFINITION_OUTPUT_BICEP_PREFIX}{Path(str(self.arm_template_path)).stem}" + self.folder_name = self.config.build_output_folder_name self._bicep_path = os.path.join(self.folder_name, self.bicep_template_name) + self._manifest_path = os.path.join( + self.folder_name, self.manifest_template_name + ) def generate_nfd(self) -> None: """Generate a VNF NFD which comprises an group, an Artifact Manifest and a NFDV.""" - # assert isinstance(self.config, VNFConfiguration) if self.bicep_path: print(f"Using the existing NFD bicep template {self.bicep_path}.") print( @@ -59,15 +60,8 @@ def generate_nfd(self) -> None: self.write() def write(self) -> None: - """ - Create a bicep template for an NFD from the ARM template for the VNF. - - :param arm_template_path: The path to the ARM template for deploying the VNF. - :param nf_name: The name of the NF. - - :return: Path to the bicep file. - """ - logger.info("Generate NFD bicep template for %s", self.arm_template_path) + """Create a bicep template for an NFD from the ARM template for the VNF.""" + logger.info(f"Generate NFD bicep template for {self.arm_template_path}") print(f"Generate NFD bicep template for {self.arm_template_path}") self._create_nfd_folder() @@ -83,6 +77,14 @@ def bicep_path(self) -> Optional[str]: return None + @property + def manifest_path(self) -> Optional[str]: + """Returns the path to the bicep file for the NFD if it has been created.""" + if os.path.exists(self._manifest_path): + return self._manifest_path + + return None + def _create_nfd_folder(self) -> None: """ Create the folder for the NFD bicep files. @@ -147,7 +149,7 @@ def write_deployment_parameters(self, folder_path: str) -> None: with open(deployment_parameters_path, "w") as _file: _file.write(json.dumps(deploy_parameters_full, indent=4)) - logger.debug("%s created", deployment_parameters_path) + logger.debug(f"{deployment_parameters_path} created") def write_template_parameters(self, folder_path: str) -> None: """ @@ -165,7 +167,7 @@ def write_template_parameters(self, folder_path: str) -> None: with open(template_parameters_path, "w") as _file: _file.write(json.dumps(template_parameters, indent=4)) - logger.debug("%s created", template_parameters_path) + logger.debug(f"{template_parameters_path} created") def write_vhd_parameters(self, folder_path: str) -> None: """ @@ -192,18 +194,14 @@ def write_vhd_parameters(self, folder_path: str) -> None: with open(vhd_parameters_path, "w", encoding="utf-8") as _file: _file.write(json.dumps(vhd_parameters, indent=4)) - logger.debug("%s created", vhd_parameters_path) + logger.debug(f"{vhd_parameters_path} created") def copy_bicep(self) -> None: - """ - Copy the bicep template into place. - - :param folder_name: The name of the folder to copy the bicep template to. - - :returns: Path to the bicep file - """ + """Copy the bicep templates into the build output folder.""" code_dir = os.path.dirname(__file__) bicep_path = os.path.join(code_dir, "templates", self.bicep_template_name) + manifest_path = os.path.join(code_dir, "templates", self.manifest_template_name) shutil.copy(bicep_path, self.folder_name) + shutil.copy(manifest_path, self.folder_name) diff --git a/src/aosm/azext_aosm/publisher_resources/publisher_resources.py b/src/aosm/azext_aosm/publisher_resources/publisher_resources.py deleted file mode 100644 index c2a93b785db..00000000000 --- a/src/aosm/azext_aosm/publisher_resources/publisher_resources.py +++ /dev/null @@ -1,31 +0,0 @@ -# -------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT -# License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------- -"""Shared publisher resources.""" -from dataclasses import dataclass -from knack.log import get_logger -from azext_aosm.vendored_sdks.models import NetworkFunctionDefinitionGroup -from azext_aosm._configuration import Configuration - - -logger = get_logger(__name__) - - -@dataclass -class PublisherResourceGenerator: - """Class for generating publisher resources used by various other classes.""" - - config: Configuration - - def generate_nfd_group(self) -> NetworkFunctionDefinitionGroup: - """ - Generate a NFD group with location and description from config. - - :return: _description_ - :rtype: NetworkFunctionDefinitionGroup - """ - return NetworkFunctionDefinitionGroup( - location=self.config.location, - description=f"NFD Group for versions of NFDs for {self.config.nf_name}", - ) diff --git a/src/aosm/azext_aosm/test.py b/src/aosm/azext_aosm/test.py deleted file mode 100644 index 4c914d0dc3c..00000000000 --- a/src/aosm/azext_aosm/test.py +++ /dev/null @@ -1,38 +0,0 @@ -from azext_aosm.vendored_sdks import HybridNetworkManagementClient -from azext_aosm.vendored_sdks.models import ( - NetworkFunctionDefinitionVersion, - NetworkFunctionDefinitionGroup, - ArtifactManifest, - ManifestArtifactFormat, - VersionState, - NetworkFunctionType, - NFVIType, - ArtifactType, - VirtualNetworkFunctionDefinitionVersion, # this is actually properties, badly named - AzureCoreNetworkFunctionTemplate, - AzureCoreNetworkFunctionVhdApplication, - AzureCoreNetworkFunctionArmTemplateApplication, -) - -vnf_props = VirtualNetworkFunctionDefinitionVersion( - version_state=VersionState.PREVIEW, - deploy_parameters="TODO", - network_function_template=AzureCoreNetworkFunctionTemplate( - network_function_applications=[ - AzureCoreNetworkFunctionVhdApplication(), - AzureCoreNetworkFunctionArmTemplateApplication(), - ] - ), -) - -# test_dict = dict(**vnf_props) -print(vnf_props.__dict__) - -nfdv = NetworkFunctionDefinitionVersion( - location="uksouth", - # network_function_type="VirtualNetworkFunction", - # Think kwargs map magically to properties in bicep, somehow - **vnf_props.__dict__ -) - -print(nfdv) diff --git a/src/aosm/azext_aosm/_constants.py b/src/aosm/azext_aosm/util/constants.py similarity index 91% rename from src/aosm/azext_aosm/_constants.py rename to src/aosm/azext_aosm/util/constants.py index 48425c52d88..61981d15241 100644 --- a/src/aosm/azext_aosm/_constants.py +++ b/src/aosm/azext_aosm/util/constants.py @@ -4,8 +4,6 @@ # -------------------------------------------------------------------------------------------- """Constants used across aosm cli extension.""" -AOSM_API_VERSION = "2022-09-01-preview" - # The types of definition that can be generated VNF = "vnf" CNF = "cnf" @@ -13,6 +11,7 @@ # Names of files used in the repo VNF_DEFINITION_BICEP_SOURCE_TEMPLATE = "vnfdefinition.bicep" +VNF_MANIFEST_BICEP_SOURCE_TEMPLATE = "vnfartifactmanifests.bicep" VNF_DEFINITION_OUTPUT_BICEP_PREFIX = "nfd-bicep-" CNF_DEFINITION_BICEP_SOURCE_TEMPLATE = "cnfdefinition.bicep" diff --git a/src/aosm/azext_aosm/util/management_clients.py b/src/aosm/azext_aosm/util/management_clients.py index 541b6686fbb..01e90443a60 100644 --- a/src/aosm/azext_aosm/util/management_clients.py +++ b/src/aosm/azext_aosm/util/management_clients.py @@ -8,137 +8,18 @@ from dataclasses import dataclass from azure.mgmt.resource import ResourceManagementClient from azext_aosm.vendored_sdks import HybridNetworkManagementClient -from typing import Dict, Optional -from azure.mgmt.resource.resources.v2022_09_01.models import Provider logger = get_logger(__name__) -@dataclass -class ProviderInfo: - """Class to return Provider Info information.""" - - namespace: str - resource_type: str - - -class ApiClientsAndCaches: - """A cache for API Clients and API versions for various resources.""" +class ApiClients: + """A class for API Clients needed throughout.""" def __init__( self, aosm_client: HybridNetworkManagementClient, resource_client: ResourceManagementClient, ): + """Initialise with clients.""" self.aosm_client = aosm_client self.resource_client = resource_client - - # We need to find an Azure API version relevant to each resource type. This is - # used in resource finding. We just use the latest and cache these as they are - # expensive to query. - self.resource_type_api_versions_cache: Dict[str, str] = {} - self.providers_cache: Dict[str, Provider] = {} - - def find_latest_api_ver_for_resource_type( - self, resource_type: str - ) -> Optional[str]: - """ - Copied from virtutils. Turns out maybe not needed yet. Expect we will need - when we want to delete resources. - - Find the latest Azure API version for a given resource. - - We do this querying the Azure Providers API - - We just use the latest and cache these as they are expensive to query. - - param: resource_type: String in the format that the providers API uses e.g. - Microsoft.Compute/disks or Microsoft.Compute/virtualMachines/extensions - - Find the namespace and resource type in the format that the providers - API uses by splitting the resource type returned from list_by_resource_group - at the first forward-slash (/), - e.g. Microsoft.Compute/disks would give us namespace Microsoft.Compute and - provider resource type disks - whereas Microsoft.Compute/virtualMachines/extensions would give us - namespace Microsoft.Compute and provicer resource type - virtualMachines/extensions. This seems to match what the provider API - uses. - - We cache values as this can take a few seconds to return. - - :param resource: A resource, as returned from list_by_resource_group - :raises RuntimeError: If no provider found in Azure for this resource - :raises RuntimeError: If the resource type is an unexpected format - """ - logger.debug(f"Find API version for {resource_type}") - # We need to find an API version relevant to the resource. - if resource_type in self.resource_type_api_versions_cache.keys(): - # We have one cached, just return that - logger.debug("Return cached API version") - return self.resource_type_api_versions_cache.get(resource_type) - - # Start with e.g. Microsoft.Compute/disks (resource_type) - assert resource_type is not None - prov_info = self.get_provider_info(resource_type) - # We now have Microsoft.Compute and disks - if prov_info.namespace not in self.providers_cache.keys(): - # Get the provider e.g. Microsoft.Compute - logger.debug(f"Find provider {prov_info.namespace}") - try: - provider = self.resource_client.providers.get(prov_info.namespace) - except Exception as provEx: - raise RuntimeError( - f"Could not find provider {prov_info.namespace} required " - f"to query resource of type {resource_type}. Aborting" - ) from provEx - - self.providers_cache[prov_info.namespace] = provider - else: - # Resource type that we haven't found before but the provider is cached - # so use that. - provider = self.providers_cache[prov_info.namespace] - - # Iterate through the providers resource types and find the one - # we want, e.g. disks or virtualMachines/extensions - for res_type in provider.resource_types: - if res_type.resource_type == prov_info.resource_type: - # Find the latest API version and cache it - # The first index appears to always be the latest version - api_version = res_type.api_versions[0] - logger.debug(f"Use API version {api_version} for {resource_type}") - - assert resource_type is not None - self.resource_type_api_versions_cache[resource_type] = api_version - return api_version - - raise RuntimeError( - f"Azure API did not return an API version for {resource_type}." - f"Cannot query API version" - ) - - def get_provider_info(self, resource_type: str) -> ProviderInfo: - """ - Find provider namespace and resource_type, given a full resource_type. - - param: resource_type: String in the format that the providers API uses e.g. - Microsoft.Compute/disks or Microsoft.Compute/virtualMachines/extensions - - Find the namespace and resource type in the format that the providers - API uses by splitting the resource type returned from list_by_resource_group - at the first forward-slash (/), - e.g. Microsoft.Compute/disks would give us namespace Microsoft.Compute and - provider resource type disks - whereas Microsoft.Compute/virtualMachines/extensions would give us - namespace Microsoft.Compute and provicer resource type - virtualMachines/extensions. This seems to match what the provider API - uses. - """ - prov_namespace_type = resource_type.split("/", 1) - if len(prov_namespace_type) != 2: - raise RuntimeError( - f"Azure resource type {resource_type} " - "is in unexpected format. Cannot find API version." - ) - # print(f"Namespace {prov_namespace_type[0]} type {prov_namespace_type[1]}") - return ProviderInfo(prov_namespace_type[0], prov_namespace_type[1]) diff --git a/src/aosm/setup.md b/src/aosm/setup.md new file mode 100644 index 00000000000..1930f5f1246 --- /dev/null +++ b/src/aosm/setup.md @@ -0,0 +1,49 @@ +### Prerequisites + +1. `python 3.8+` + + +### Dev environment setup + +Follow [https://github.com/Azure/azure-cli-dev-tools](https://github.com/Azure/azure-cli-dev-tools) + +Clone both azure-cli and azure-cli-extensions +```bash +# Go into your git clone of az-cli-extensions +cd az-cli-extensions + +# Create a virtual environment to run in +python3.8 -m venv ~/.virtualenvs/az-cli-env +source ~/.virtualenvs/az-cli-env/bin/activate + +# Ensure you have pip +python -m pip install -U pip + +# Install azdev +pip install azdev + +# Install all the python dependencies you need +azdev setup --cli /home/developer/code/azure-cli --repo . + +# Add the extension to your local CLI +azdev extension add aosm +``` +### Generating the AOSM Python SDK +TODO + +### VSCode environment setup. + +Make sure your VSCode is running in the same python virtual environment + +### Linting and Tests +```bash +azdev style aosm +azdev linter --include-whl-extensions aosm +(Not written any tests yet) +azdev test aosm +``` +You can use python-static-checks in your dev environment if you want, to help you: +```bash +pip3 install -U --index-url https://pkgs.dev.azure.com/msazuredev/AzureForOperators/_packaging/python/pypi/simple/ python-static-checks==4.0.0 +python-static-checks fmt +```