diff --git a/src/aosm/azext_aosm/_configuration.py b/src/aosm/azext_aosm/_configuration.py index cb3a5174ff2..cf76bdea7c8 100644 --- a/src/aosm/azext_aosm/_configuration.py +++ b/src/aosm/azext_aosm/_configuration.py @@ -7,7 +7,7 @@ import logging import json import os -from dataclasses import dataclass, field +from dataclasses import dataclass, field, asdict from pathlib import Path from typing import Any, Dict, List, Optional, Union @@ -23,108 +23,87 @@ logger = logging.getLogger(__name__) -DESCRIPTION_MAP: Dict[str, str] = { - "publisher_resource_group_name": ( - "Resource group for the Publisher resource. " - "Will be created if it does not exist." - ), - "publisher_name": ( - "Name of the Publisher resource you want your definition published to. " - "Will be created if it does not exist." - ), - "publisher_resource_group_name_nsd": "Resource group for the Publisher resource.", - "nf_name": "Name of NF definition", - "version": "Version of the NF definition in A.B.C format.", - "acr_artifact_store_name": ( - "Name of the ACR Artifact Store resource. Will be created if it does not exist." - ), - "location": "Azure location to use when creating resources.", - "blob_artifact_store_name": ( - "Name of the storage account Artifact Store resource. Will be created if it " - "does not exist." - ), - "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. Relative paths are relative to the configuration file." - "On Windows escape any backslash with another backslash." - ), - "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" - ), - "nsdv_description": "Description of the NSDV", - "nsd_name": ( - "Network Service Design (NSD) name. This is the collection of Network Service" - " Design Versions. Will be created if it does not exist." - ), - "nsd_version": ( - "Version of the NSD to be created. This should be in the format A.B.C" - ), - "helm_package_name": "Name of the Helm package", - "path_to_chart": ( - "File path of Helm Chart on local disk. Accepts .tgz, .tar or .tar.gz." - " Use Linux slash (/) file separator even if running on Windows." - ), - "path_to_mappings": ( - "File path of value mappings on local disk where chosen values are replaced " - "with deploymentParameter placeholders. Accepts .yaml or .yml. If left as a " - "blank string, a value mappings file will be generated with every value " - "mapped to a deployment parameter. Use a blank string and --interactive on " - "the build command to interactively choose which values to map." - ), - "helm_depends_on": ( - "Names of the Helm packages this package depends on. " - "Leave as an empty array if no dependencies" - ), - "image_name_parameter": ( - "The parameter name in the VM ARM template which specifies the name of the " - "image to use for the VM." - ), - "source_registry": ( - "Optional. Login server of the source acr registry from which to pull the " - "image(s). For example sourceacr.azurecr.io. Leave blank if you have set " - "source_local_docker_image." - ), - "source_local_docker_image": ( - "Optional. Image name of the source docker image from local machine. For " - "limited use case where the CNF only requires a single docker image and exists " - "in the local docker repository. Set to blank of not required." - ), - "source_registry_namespace": ( - "Optional. Namespace of the repository of the source acr registry from which " - "to pull. For example if your repository is samples/prod/nginx then set this to" - " samples/prod . Leave blank if the image is in the root namespace or you have " - "set source_local_docker_image." - "See https://learn.microsoft.com/en-us/azure/container-registry/" - "container-registry-best-practices#repository-namespaces for further details." - ), -} - @dataclass class ArtifactConfig: # artifact.py checks for the presence of the default descriptions, change # there if you change the descriptions. - artifact_name: str = DESCRIPTION_MAP["artifact_name"] - file_path: Optional[str] = DESCRIPTION_MAP["file_path"] - blob_sas_url: Optional[str] = DESCRIPTION_MAP["blob_sas_url"] - version: Optional[str] = DESCRIPTION_MAP["artifact_version"] + artifact_name: str = "" + file_path: Optional[str] = None + blob_sas_url: Optional[str] = None + version: Optional[str] = "" + + @classmethod + def helptext(cls) -> "ArtifactConfig": + """ + Build an object where each value is helptext for that field. + """ + return ArtifactConfig( + artifact_name="Optional. Name of the artifact.", + file_path=( + "Optional. File path of the artifact you wish to upload from your local disk. " + "Delete if not required. Relative paths are relative to the configuration file." + "On Windows escape any backslash with another backslash." + ), + blob_sas_url=( + "Optional. SAS URL of the blob artifact you wish to copy to your Artifact" + " Store. Delete if not required." + ), + version="Version of the artifact in A.B.C format.", + ) + + def validate(self): + """ + Validate the configuration. + """ + if not self.version: + raise ValidationError("version must be set.") + if self.blob_sas_url and self.file_path: + raise ValidationError("Only one of file_path or blob_sas_url may be set.") + if not (self.blob_sas_url or self.file_path): + raise ValidationError("One of file_path or sas_blob_url must be set.") @dataclass class Configuration(abc.ABC): config_file: Optional[str] = None - publisher_name: str = DESCRIPTION_MAP["publisher_name"] - publisher_resource_group_name: str = DESCRIPTION_MAP[ - "publisher_resource_group_name" - ] - acr_artifact_store_name: str = DESCRIPTION_MAP["acr_artifact_store_name"] - location: str = DESCRIPTION_MAP["location"] + publisher_name: str = "" + publisher_resource_group_name: str = "" + acr_artifact_store_name: str = "" + location: str = "" + + @classmethod + def helptext(cls): + """ + Build an object where each value is helptext for that field. + """ + return Configuration( + publisher_name=( + "Name of the Publisher resource you want your definition published to. " + "Will be created if it does not exist." + ), + publisher_resource_group_name=( + "Resource group for the Publisher resource. " + "Will be created if it does not exist." + ), + acr_artifact_store_name=( + "Name of the ACR Artifact Store resource. Will be created if it does not exist." + ), + location="Azure location to use when creating resources.", + ) + + def validate(self): + """ + Validate the configuration. + """ + if not self.location: + raise ValidationError("Location must be set") + if not self.publisher_name: + raise ValidationError("Publisher name must be set") + if not self.publisher_resource_group_name: + raise ValidationError("Publisher resource group name must be set") + if not self.acr_artifact_store_name: + raise ValidationError("ACR Artifact Store name must be set") def path_from_cli_dir(self, path: str) -> str: """ @@ -169,14 +148,29 @@ def acr_manifest_names(self) -> List[str]: class NFConfiguration(Configuration): """Network Function configuration.""" - 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"] + nf_name: str = "" + version: str = "" + + @classmethod + def helptext(cls) -> "NFConfiguration": + """ + Build an object where each value is helptext for that field. + """ + return NFConfiguration( + nf_name="Name of NF definition", + version="Version of the NF definition in A.B.C format.", + **asdict(Configuration.helptext()), + ) + + def validate(self): + """ + Validate the configuration. + """ + super().validate() + if not self.nf_name: + raise ValidationError("nf_name must be set") + if not self.version: + raise ValidationError("version must be set") @property def nfdg_name(self) -> str: @@ -197,10 +191,29 @@ def acr_manifest_names(self) -> List[str]: @dataclass class VNFConfiguration(NFConfiguration): - blob_artifact_store_name: str = DESCRIPTION_MAP["blob_artifact_store_name"] - image_name_parameter: str = DESCRIPTION_MAP["image_name_parameter"] - arm_template: Any = ArtifactConfig() - vhd: Any = ArtifactConfig() + blob_artifact_store_name: str = "" + image_name_parameter: str = "" + arm_template: Union[Dict[str, str], ArtifactConfig] = ArtifactConfig() + vhd: Union[Dict[str, str], ArtifactConfig] = ArtifactConfig() + + @classmethod + def helptext(cls) -> "VNFConfiguration": + """ + Build an object where each value is helptext for that field. + """ + return VNFConfiguration( + blob_artifact_store_name=( + "Name of the storage account Artifact Store resource. Will be created if it " + "does not exist." + ), + image_name_parameter=( + "The parameter name in the VM ARM template which specifies the name of the " + "image to use for the VM." + ), + arm_template=ArtifactConfig.helptext(), + vhd=ArtifactConfig.helptext(), + **asdict(NFConfiguration.helptext()), + ) def __post_init__(self): """ @@ -218,7 +231,6 @@ def __post_init__(self): if self.vhd.get("file_path"): self.vhd["file_path"] = self.path_from_cli_dir(self.vhd["file_path"]) self.vhd = ArtifactConfig(**self.vhd) - self.validate() def validate(self) -> None: """ @@ -226,10 +238,15 @@ def validate(self) -> None: :raises ValidationError for any invalid config """ + super().validate() + + assert isinstance(self.vhd, ArtifactConfig) + assert isinstance(self.arm_template, ArtifactConfig) + self.vhd.validate() + self.arm_template.validate() - if self.vhd.version == DESCRIPTION_MAP["version"]: - # Config has not been filled in. Don't validate. - return + assert self.vhd.version + assert self.arm_template.version if "." in self.vhd.version or "-" not in self.vhd.version: raise ValidationError( @@ -241,26 +258,6 @@ def validate(self) -> None: "Config validation error. ARM template artifact version should be in" " format A.B.C" ) - filepath_set = ( - self.vhd.file_path and self.vhd.file_path != DESCRIPTION_MAP["file_path"] - ) - sas_set = ( - self.vhd.blob_sas_url - and self.vhd.blob_sas_url != DESCRIPTION_MAP["blob_sas_url"] - ) - # If these are the same, either neither is set or both are, both of which are errors - if filepath_set == sas_set: - raise ValidationError( - "Config validation error. VHD config must have either a local filepath" - " or a blob SAS URL" - ) - - if filepath_set: - # Explicitly set the blob SAS URL to None to avoid other code having to - # check if the value is the default description - self.vhd.blob_sas_url = None - elif sas_set: - self.vhd.file_path = None @property def sa_manifest_name(self) -> str: @@ -271,18 +268,51 @@ def sa_manifest_name(self) -> str: @property def output_directory_for_build(self) -> Path: """Return the local folder for generating the bicep template to.""" + assert isinstance(self.arm_template, ArtifactConfig) + assert self.arm_template.file_path arm_template_name = Path(self.arm_template.file_path).stem return Path(f"{NF_DEFINITION_OUTPUT_BICEP_PREFIX}{arm_template_name}") @dataclass class HelmPackageConfig: - name: str = DESCRIPTION_MAP["helm_package_name"] - path_to_chart: str = DESCRIPTION_MAP["path_to_chart"] - path_to_mappings: str = DESCRIPTION_MAP["path_to_mappings"] - depends_on: List[str] = field( - default_factory=lambda: [DESCRIPTION_MAP["helm_depends_on"]] - ) + name: str = "" + path_to_chart: str = "" + path_to_mappings: str = "" + depends_on: List[str] = field(default_factory=lambda: []) + + @classmethod + def helptext(cls): + """ + Build an object where each value is helptext for that field. + """ + return HelmPackageConfig( + name="Name of the Helm package", + path_to_chart=( + "File path of Helm Chart on local disk. Accepts .tgz, .tar or .tar.gz." + " Use Linux slash (/) file separator even if running on Windows." + ), + path_to_mappings=( + "File path of value mappings on local disk where chosen values are replaced " + "with deploymentParameter placeholders. Accepts .yaml or .yml. If left as a " + "blank string, a value mappings file will be generated with every value " + "mapped to a deployment parameter. Use a blank string and --interactive on " + "the build command to interactively choose which values to map." + ), + depends_on=( + "Names of the Helm packages this package depends on. " + "Leave as an empty array if no dependencies" + ), + ) + + def validate(self): + """ + Validate the configuration. + """ + if not self.name: + raise ValidationError("name must be set") + if not self.path_to_chart: + raise ValidationError("path_to_chart must be set") @dataclass @@ -293,30 +323,59 @@ class CNFImageConfig: source_registry_namespace: str = "" source_local_docker_image: str = "" - def __post_init__(self): + @classmethod + def helptext(cls) -> "CNFImageConfig": """ - Cope with optional parameters being omitted in the loaded json config file. + Build an object where each value is helptext for that field. + """ + return CNFImageConfig( + source_registry=( + "Optional. Login server of the source acr registry from which to pull the " + "image(s). For example sourceacr.azurecr.io. Leave blank if you have set " + "source_local_docker_image." + ), + source_registry_namespace=( + "Optional. Namespace of the repository of the source acr registry from which " + "to pull. For example if your repository is samples/prod/nginx then set this to" + " samples/prod . Leave blank if the image is in the root namespace or you have " + "set source_local_docker_image." + "See https://learn.microsoft.com/en-us/azure/container-registry/" + "container-registry-best-practices#repository-namespaces for further details." + ), + source_local_docker_image=( + "Optional. Image name of the source docker image from local machine. For " + "limited use case where the CNF only requires a single docker image and exists " + "in the local docker repository. Set to blank of not required." + ), + ) - If param is set to placeholder text, it is not in the config file and should be unset. + def validate(self): + """ + Validate the configuration. """ - if self.source_registry == DESCRIPTION_MAP["source_registry"]: - self.source_registry = "" - if ( - self.source_registry_namespace - == DESCRIPTION_MAP["source_registry_namespace"] - ): - self.source_registry_namespace = "" - if ( - self.source_local_docker_image - == DESCRIPTION_MAP["source_local_docker_image"] - ): - self.source_local_docker_image = "" + if self.source_registry_namespace and not self.source_registry: + raise ValidationError( + "Config validation error. The image source registry namespace should " + "only be configured if a source registry is configured." + ) + + if self.source_registry and self.source_local_docker_image: + raise ValidationError( + "Only one of source_registry and source_local_docker_image can be set." + ) + + if not (self.source_registry or self.source_local_docker_image): + raise ValidationError( + "One of source_registry or source_local_docker_image must be set." + ) @dataclass class CNFConfiguration(NFConfiguration): - images: Any = CNFImageConfig() - helm_packages: List[Any] = field(default_factory=lambda: [HelmPackageConfig()]) + images: Union[Dict[str, str], CNFImageConfig] = CNFImageConfig() + helm_packages: List[Union[Dict[str, Any], HelmPackageConfig]] = field( + default_factory=lambda: [] + ) def __post_init__(self): """ @@ -335,7 +394,17 @@ def __post_init__(self): self.helm_packages[package_index] = HelmPackageConfig(**dict(package)) if isinstance(self.images, dict): self.images = CNFImageConfig(**self.images) - self.validate() + + @classmethod + def helptext(cls) -> "CNFConfiguration": + """ + Build an object where each value is helptext for that field. + """ + return CNFConfiguration( + images=CNFImageConfig.helptext(), + helm_packages=[HelmPackageConfig.helptext()], + **asdict(NFConfiguration.helptext()), + ) @property def output_directory_for_build(self) -> Path: @@ -348,56 +417,65 @@ def validate(self): :raises ValidationError: If source registry ID doesn't match the regex """ + assert isinstance(self.images, CNFImageConfig) + super().validate() - source_reg_set = self.images.source_registry != "" - source_local_set = self.images.source_local_docker_image != "" - source_reg_namespace_set = self.images.source_registry_namespace != "" - - if source_reg_namespace_set and not source_reg_set: - raise ValidationError( - "Config validation error. The image source registry namespace should " - "only be configured if a source registry is configured." - ) - # If these are the same, either neither is set or both are, both of which are errors - if source_reg_set == source_local_set: - raise ValidationError( - "Config validation error. Images config must have either a local docker image" - " or a source registry, but not both." - ) - + self.images.validate() -NFD_NAME = "The name of the existing Network Function Definition Group to deploy using this NSD" -NFD_VERSION = ( - "The version of the existing Network Function Definition to base this NSD on. " - "This NSD will be able to deploy any NFDV with deployment parameters compatible " - "with this version." -) -NFD_LOCATION = "The region that the NFDV is published to." -PUBLISHER_RESOURCE_GROUP = "The resource group that the publisher is hosted in." -PUBLISHER_NAME = "The name of the publisher that this NFDV is published under." -PUBLISHER_SCOPE = ( - "The scope that the publisher is published under. Only 'private' is supported." -) -NFD_TYPE = "Type of Network Function. Valid values are 'cnf' or 'vnf'" -MULTIPLE_INSTANCES = ( - "Set to true or false. Whether the NSD should allow arbitrary numbers of this " - "type of NF. If set to false only a single instance will be allowed. Only " - "supported on VNFs, must be set to false on CNFs." -) + for helm_package in self.helm_packages: + assert isinstance(helm_package, HelmPackageConfig) + helm_package.validate() @dataclass class NFDRETConfiguration: # pylint: disable=too-many-instance-attributes """The configuration required for an NFDV that you want to include in an NSDV.""" - publisher: str = PUBLISHER_NAME - publisher_resource_group: str = PUBLISHER_RESOURCE_GROUP - name: str = NFD_NAME - version: str = NFD_VERSION - publisher_offering_location: str = NFD_LOCATION - publisher_scope: str = PUBLISHER_SCOPE - type: str = NFD_TYPE - multiple_instances: Union[str, bool] = MULTIPLE_INSTANCES + publisher: str = "" + publisher_resource_group: str = "" + name: str = "" + version: str = "" + publisher_offering_location: str = "" + publisher_scope: str = "" + type: str = "" + multiple_instances: Union[str, bool] = False + + def __post_init__(self): + """ + Convert parameters to the correct types. + """ + # Cope with multiple_instances being supplied as a string, rather than a bool. + if isinstance(self.multiple_instances, str): + if self.multiple_instances.lower() == "true": + self.multiple_instances = True + elif self.multiple_instances.lower() == "false": + self.multiple_instances = False + + @classmethod + def helptext(cls) -> "NFDRETConfiguration": + """ + Build an object where each value is helptext for that field. + """ + return NFDRETConfiguration( + publisher="The name of the existing Network Function Definition Group to deploy using this NSD", + publisher_resource_group="The resource group that the publisher is hosted in.", + name="The name of the existing Network Function Definition Group to deploy using this NSD", + version=( + "The version of the existing Network Function Definition to base this NSD on. " + "This NSD will be able to deploy any NFDV with deployment parameters compatible " + "with this version." + ), + publisher_offering_location="The region that the NFDV is published to.", + publisher_scope=( + "The scope that the publisher is published under. Only 'private' is supported." + ), + type="Type of Network Function. Valid values are 'cnf' or 'vnf'", + multiple_instances=( + "Set to true or false. Whether the NSD should allow arbitrary numbers of this " + "type of NF. If set to false only a single instance will be allowed. Only " + "supported on VNFs, must be set to false on CNFs." + ), + ) def validate(self) -> None: """ @@ -405,28 +483,28 @@ def validate(self) -> None: :raises ValidationError for any invalid config """ - if self.name == NFD_NAME: + if not self.name: raise ValidationError("Network function definition name must be set") - if self.publisher == PUBLISHER_NAME: + if not self.publisher: raise ValidationError(f"Publisher name must be set for {self.name}") - if self.publisher_resource_group == PUBLISHER_RESOURCE_GROUP: + if not self.publisher_resource_group: raise ValidationError( f"Publisher resource group name must be set for {self.name}" ) - if self.version == NFD_VERSION: + if not self.version: raise ValidationError( f"Network function definition version must be set for {self.name}" ) - if self.publisher_offering_location == NFD_LOCATION: + if not self.publisher_offering_location: raise ValidationError( f"Network function definition offering location must be set, for {self.name}" ) - if self.publisher_scope == PUBLISHER_SCOPE: + if not self.publisher_scope: raise ValidationError( f"Network function definition publisher scope must be set, for {self.name}" ) @@ -493,14 +571,12 @@ def acr_manifest_name(self, nsd_version: str) -> str: @dataclass class NSConfiguration(Configuration): - network_functions: List[NFDRETConfiguration] = field( - default_factory=lambda: [ - NFDRETConfiguration(), - ] + network_functions: List[Union[NFDRETConfiguration, Dict[str, Any]]] = field( + default_factory=lambda: [] ) - nsd_name: str = DESCRIPTION_MAP["nsd_name"] - nsd_version: str = DESCRIPTION_MAP["nsd_version"] - nsdv_description: str = DESCRIPTION_MAP["nsdv_description"] + nsd_name: str = "" + nsd_version: str = "" + nsdv_description: str = "" def __post_init__(self): """Covert things to the correct format.""" @@ -510,36 +586,42 @@ def __post_init__(self): ] self.network_functions = nf_ret_list + @classmethod + def helptext(cls) -> "NSConfiguration": + """ + Build a NSConfiguration object where each value is helptext for that field. + """ + nsd_helptext = NSConfiguration( + network_functions=[asdict(NFDRETConfiguration.helptext())], + nsd_name=( + "Network Service Design (NSD) name. This is the collection of Network Service" + " Design Versions. Will be created if it does not exist." + ), + nsd_version=( + "Version of the NSD to be created. This should be in the format A.B.C" + ), + nsdv_description="Description of the NSDV.", + **asdict(Configuration.helptext()), + ) + + return nsd_helptext + def validate(self): """ Validate the configuration passed in. :raises ValueError for any invalid config """ - - if self.location in (DESCRIPTION_MAP["location"], ""): - raise ValueError("Location must be set") - if self.publisher_name in (DESCRIPTION_MAP["publisher_name"], ""): - raise ValueError("Publisher name must be set") - if self.publisher_resource_group_name in ( - DESCRIPTION_MAP["publisher_resource_group_name_nsd"], - "", - ): - raise ValueError("Publisher resource group name must be set") - if self.acr_artifact_store_name in ( - DESCRIPTION_MAP["acr_artifact_store_name"], - "", - ): - raise ValueError("ACR Artifact Store name must be set") - if self.network_functions in ([], None): + super().validate() + if not self.network_functions: raise ValueError(("At least one network function must be included.")) for configuration in self.network_functions: configuration.validate() - if self.nsd_name in (DESCRIPTION_MAP["nsd_name"], ""): - raise ValueError("NSD name must be set") - if self.nsd_version in (DESCRIPTION_MAP["nsd_version"], ""): - raise ValueError("NSD Version must be set") + if not self.nsd_name: + raise ValueError("nsd_name must be set") + if not self.nsd_version: + raise ValueError("nsd_version must be set") @property def output_directory_for_build(self) -> Path: @@ -560,12 +642,16 @@ def cg_schema_name(self) -> str: @property def acr_manifest_names(self) -> List[str]: """The list of ACR manifest names for all the NF ARM templates.""" - return [nf.acr_manifest_name(self.nsd_version) for nf in self.network_functions] + acr_manifest_names = [] + for nf in self.network_functions: + assert isinstance(nf, NFDRETConfiguration) + acr_manifest_names.append(nf.acr_manifest_name(self.nsd_version)) + logger.debug("ACR manifest names: %s", acr_manifest_names) + return acr_manifest_names -def get_configuration( - configuration_type: str, config_file: Optional[str] = None -) -> Configuration: + +def get_configuration(configuration_type: str, config_file: str) -> Configuration: """ Return the correct configuration object based on the type. @@ -573,16 +659,13 @@ def get_configuration( :param config_file: The path to the config file :return: The configuration object """ - if config_file: - try: - with open(config_file, "r", encoding="utf-8") as f: - config_as_dict = json.loads(f.read()) - except json.decoder.JSONDecodeError as e: - raise InvalidArgumentValueError( - f"Config file {config_file} is not valid JSON: {e}" - ) from e - else: - config_as_dict = {} + try: + with open(config_file, "r", encoding="utf-8") as f: + config_as_dict = json.loads(f.read()) + except json.decoder.JSONDecodeError as e: + raise InvalidArgumentValueError( + f"Config file {config_file} is not valid JSON: {e}" + ) from e config: Configuration try: @@ -601,4 +684,6 @@ def get_configuration( f"Config file {config_file} is not valid: {typeerr}" ) from typeerr + config.validate() + return config diff --git a/src/aosm/azext_aosm/custom.py b/src/aosm/azext_aosm/custom.py index 736da37c73b..dee5efb2e2b 100644 --- a/src/aosm/azext_aosm/custom.py +++ b/src/aosm/azext_aosm/custom.py @@ -89,9 +89,14 @@ def generate_definition_config(definition_type: str, output_file: str = "input.j :param definition_type: CNF, VNF :param output_file: path to output config file, defaults to "input.json" - :type output_file: str, optional """ - _generate_config(configuration_type=definition_type, output_file=output_file) + config: Configuration + if definition_type == CNF: + config = CNFConfiguration.helptext() + elif definition_type == VNF: + config = VNFConfiguration.helptext() + + _generate_config(configuration=config, output_file=output_file) def _get_config_from_file(config_file: str, configuration_type: str) -> Configuration: @@ -324,21 +329,21 @@ def generate_design_config(output_file: str = "input.json"): :param output_file: path to output config file, defaults to "input.json" :type output_file: str, optional """ - _generate_config(NSD, output_file) + _generate_config(NSConfiguration.helptext(), output_file) -def _generate_config(configuration_type: str, output_file: str = "input.json"): +def _generate_config(configuration: Configuration, output_file: str = "input.json"): """ Generic generate config function for NFDs and NSDs. - :param configuration_type: CNF, VNF or NSD + :param configuration: The Configuration object with helptext filled in for each of + the fields. :param output_file: path to output config file, defaults to "input.json" - :type output_file: str, optional """ # Config file is a special parameter on the configuration objects. It is the path # to the configuration file, rather than an input parameter. It therefore shouldn't # be included here. - config = asdict(get_configuration(configuration_type)) + config = asdict(configuration) config.pop("config_file") config_as_dict = json.dumps(config, indent=4) @@ -353,10 +358,10 @@ def _generate_config(configuration_type: str, output_file: str = "input.json"): with open(output_file, "w", encoding="utf-8") as f: f.write(config_as_dict) - if configuration_type in (CNF, VNF): - prtName = "definition" - else: + if isinstance(configuration, NSConfiguration): prtName = "design" + else: + prtName = "definition" print(f"Empty {prtName} configuration has been written to {output_file}") logger.info( "Empty %s configuration has been written to %s", prtName, output_file diff --git a/src/aosm/azext_aosm/deploy/deploy_with_arm.py b/src/aosm/azext_aosm/deploy/deploy_with_arm.py index 453fb4f50e8..2a3b7ef03c1 100644 --- a/src/aosm/azext_aosm/deploy/deploy_with_arm.py +++ b/src/aosm/azext_aosm/deploy/deploy_with_arm.py @@ -18,9 +18,11 @@ from knack.util import CLIError from azext_aosm._configuration import ( + ArtifactConfig, CNFConfiguration, Configuration, NFConfiguration, + NFDRETConfiguration, NSConfiguration, VNFConfiguration, ) @@ -179,14 +181,20 @@ def _vnfd_artifact_upload(self) -> None: vhd_artifact = storage_account_manifest.artifacts[0] arm_template_artifact = acr_manifest.artifacts[0] + vhd_config = self.config.vhd + arm_template_config = self.config.arm_template + + assert isinstance(vhd_config, ArtifactConfig) + assert isinstance(arm_template_config, ArtifactConfig) + if self.skip == IMAGE_UPLOAD: print("Skipping VHD artifact upload") else: print("Uploading VHD artifact") - vhd_artifact.upload(self.config.vhd) + vhd_artifact.upload(vhd_config) print("Uploading ARM template artifact") - arm_template_artifact.upload(self.config.arm_template) + arm_template_artifact.upload(arm_template_config) def _cnfd_artifact_upload(self) -> None: """Uploads the Helm chart and any additional images.""" @@ -302,6 +310,8 @@ def construct_parameters(self) -> Dict[str, Any]: """ if self.resource_type == VNF: assert isinstance(self.config, VNFConfiguration) + assert isinstance(self.config.vhd, ArtifactConfig) + assert isinstance(self.config.arm_template, ArtifactConfig) return { "location": {"value": self.config.location}, "publisherName": {"value": self.config.publisher_name}, @@ -341,6 +351,8 @@ def construct_manifest_parameters(self) -> Dict[str, Any]: """Create the parmeters dictionary for VNF, CNF or NSD.""" if self.resource_type == VNF: assert isinstance(self.config, VNFConfiguration) + assert isinstance(self.config.vhd, ArtifactConfig) + assert isinstance(self.config.arm_template, ArtifactConfig) return { "location": {"value": self.config.location}, "publisherName": {"value": self.config.publisher_name}, @@ -363,9 +375,11 @@ def construct_manifest_parameters(self) -> Dict[str, Any]: if self.resource_type == NSD: assert isinstance(self.config, NSConfiguration) - arm_template_names = [ - nf.arm_template.artifact_name for nf in self.config.network_functions - ] + arm_template_names = [] + + for nf in self.config.network_functions: + assert isinstance(nf, NFDRETConfiguration) + arm_template_names.append(nf.arm_template.artifact_name) # Set the artifact version to be the same as the NSD version, so that they # don't get over written when a new NSD is published. @@ -417,6 +431,7 @@ def deploy_nsd_from_bicep(self) -> None: for manifest, nf in zip( self.config.acr_manifest_names, self.config.network_functions ): + assert isinstance(nf, NFDRETConfiguration) acr_manifest = ArtifactManifestOperator( self.config, self.api_clients, diff --git a/src/aosm/azext_aosm/deploy/pre_deploy.py b/src/aosm/azext_aosm/deploy/pre_deploy.py index 867deb80402..e1d01e115f4 100644 --- a/src/aosm/azext_aosm/deploy/pre_deploy.py +++ b/src/aosm/azext_aosm/deploy/pre_deploy.py @@ -418,9 +418,7 @@ def ensure_nsd_exists( network_service_design_group_name=nsd_name, parameters=NetworkServiceDesignGroup(location=location), ) - LongRunningOperation(self.cli_ctx, "Creating Network Service Design...")( - poller - ) + LongRunningOperation(self.cli_ctx, "Creating Network Service Design...")(poller) def resource_exists_by_name(self, rg_name: str, resource_name: str) -> bool: """ diff --git a/src/aosm/azext_aosm/generate_nfd/cnf_nfd_generator.py b/src/aosm/azext_aosm/generate_nfd/cnf_nfd_generator.py index 19e4cb62037..fd365d7aae2 100644 --- a/src/aosm/azext_aosm/generate_nfd/cnf_nfd_generator.py +++ b/src/aosm/azext_aosm/generate_nfd/cnf_nfd_generator.py @@ -117,6 +117,8 @@ def generate_nfd(self) -> None: try: for helm_package in self.config.helm_packages: # Unpack the chart into the tmp directory + assert isinstance(helm_package, HelmPackageConfig) + self._extract_chart(Path(helm_package.path_to_chart)) # TODO: Validate charts diff --git a/src/aosm/azext_aosm/generate_nfd/vnf_nfd_generator.py b/src/aosm/azext_aosm/generate_nfd/vnf_nfd_generator.py index 1be74bebc88..7990f3bf234 100644 --- a/src/aosm/azext_aosm/generate_nfd/vnf_nfd_generator.py +++ b/src/aosm/azext_aosm/generate_nfd/vnf_nfd_generator.py @@ -13,7 +13,7 @@ from knack.log import get_logger -from azext_aosm._configuration import VNFConfiguration +from azext_aosm._configuration import ArtifactConfig, VNFConfiguration from azext_aosm.generate_nfd.nfd_generator_base import NFDGenerator from azext_aosm.util.constants import ( CONFIG_MAPPINGS_DIR_NAME, @@ -64,6 +64,9 @@ class VnfNfdGenerator(NFDGenerator): def __init__(self, config: VNFConfiguration, order_params: bool, interactive: bool): self.config = config + assert isinstance(self.config.arm_template, ArtifactConfig) + assert self.config.arm_template.file_path + self.arm_template_path = Path(self.config.arm_template.file_path) self.output_directory: Path = self.config.output_directory_for_build diff --git a/src/aosm/azext_aosm/generate_nsd/nf_ret.py b/src/aosm/azext_aosm/generate_nsd/nf_ret.py index 4a9a0e55f2b..0ef91e0b21e 100644 --- a/src/aosm/azext_aosm/generate_nsd/nf_ret.py +++ b/src/aosm/azext_aosm/generate_nsd/nf_ret.py @@ -50,12 +50,14 @@ def _get_nfdv( "Reading existing NFDV resource object " f"{config.version} from group {config.name}" ) - nfdv_object = api_clients.aosm_client.proxy_network_function_definition_versions.get( - publisher_scope_name=config.publisher_scope, - publisher_location_name=config.publisher_offering_location, - proxy_publisher_name=config.publisher, - network_function_definition_group_name=config.name, - network_function_definition_version_name=config.version, + nfdv_object = ( + api_clients.aosm_client.proxy_network_function_definition_versions.get( + publisher_scope_name=config.publisher_scope, + publisher_location_name=config.publisher_offering_location, + proxy_publisher_name=config.publisher, + network_function_definition_group_name=config.name, + network_function_definition_version_name=config.version, + ) ) return nfdv_object diff --git a/src/aosm/azext_aosm/generate_nsd/nsd_generator.py b/src/aosm/azext_aosm/generate_nsd/nsd_generator.py index e914c7c73c3..adee03677e6 100644 --- a/src/aosm/azext_aosm/generate_nsd/nsd_generator.py +++ b/src/aosm/azext_aosm/generate_nsd/nsd_generator.py @@ -13,7 +13,7 @@ from jinja2 import Template from knack.log import get_logger -from azext_aosm._configuration import NSConfiguration +from azext_aosm._configuration import NFDRETConfiguration, NSConfiguration from azext_aosm.generate_nsd.nf_ret import NFRETGenerator from azext_aosm.util.constants import ( CONFIG_MAPPINGS_DIR_NAME, @@ -56,10 +56,14 @@ def __init__(self, api_clients: ApiClients, config: NSConfiguration): self.config = config self.nsd_bicep_template_name = NSD_DEFINITION_JINJA2_SOURCE_TEMPLATE self.nsd_bicep_output_name = NSD_BICEP_FILENAME - self.nf_ret_generators = [ - NFRETGenerator(api_clients, nf_config, self.config.cg_schema_name) - for nf_config in self.config.network_functions - ] + + self.nf_ret_generators = [] + + for nf_config in self.config.network_functions: + assert isinstance(nf_config, NFDRETConfiguration) + self.nf_ret_generators.append( + NFRETGenerator(api_clients, nf_config, self.config.cg_schema_name) + ) def generate_nsd(self) -> None: """Generate a NSD templates which includes an Artifact Manifest, NFDV and NF templates.""" diff --git a/src/aosm/azext_aosm/tests/latest/mock_nsd/input.json b/src/aosm/azext_aosm/tests/latest/mock_nsd/input.json index 94d92fe0334..78b00cf08e5 100644 --- a/src/aosm/azext_aosm/tests/latest/mock_nsd/input.json +++ b/src/aosm/azext_aosm/tests/latest/mock_nsd/input.json @@ -15,7 +15,7 @@ "publisher_scope": "private" } ], - "nsdg_name": "ubuntu", + "nsd_name": "ubuntu", "nsd_version": "1.0.0", "nsdv_description": "Plain ubuntu VM" } \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/mock_nsd/input_multi_nf_nsd.json b/src/aosm/azext_aosm/tests/latest/mock_nsd/input_multi_nf_nsd.json index 8ef686b99ec..0c5c5160ca5 100644 --- a/src/aosm/azext_aosm/tests/latest/mock_nsd/input_multi_nf_nsd.json +++ b/src/aosm/azext_aosm/tests/latest/mock_nsd/input_multi_nf_nsd.json @@ -12,7 +12,7 @@ "version": "1.0.0", "publisher_offering_location": "eastus", "type": "cnf", - "multiple_instances": false + "multiple_instances": "False" }, { "publisher": "reference-publisher", @@ -25,7 +25,7 @@ "multiple_instances": false } ], - "nsdg_name": "multinf", + "nsd_name": "multinf", "nsd_version": "1.0.1", "nsdv_description": "Test deploying multiple NFs" } diff --git a/src/aosm/azext_aosm/tests/latest/mock_nsd/input_multiple_instances.json b/src/aosm/azext_aosm/tests/latest/mock_nsd/input_multiple_instances.json index 0f2a55de152..e03f76c9c0d 100644 --- a/src/aosm/azext_aosm/tests/latest/mock_nsd/input_multiple_instances.json +++ b/src/aosm/azext_aosm/tests/latest/mock_nsd/input_multiple_instances.json @@ -9,13 +9,13 @@ "version": "1.0.0", "publisher_offering_location": "eastus", "type": "vnf", - "multiple_instances": true, + "multiple_instances": "True", "publisher_scope": "private", "publisher": "jamie-mobile-publisher", "publisher_resource_group": "Jamie-publisher" } ], - "nsdg_name": "ubuntu", + "nsd_name": "ubuntu", "nsd_version": "1.0.0", "nsdv_description": "Plain ubuntu VM" } \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/scenario_test_mocks/mock_input_templates/cnf_nsd_input_template.json b/src/aosm/azext_aosm/tests/latest/scenario_test_mocks/mock_input_templates/cnf_nsd_input_template.json index ac0822bf6f1..2a4c5622ee4 100644 --- a/src/aosm/azext_aosm/tests/latest/scenario_test_mocks/mock_input_templates/cnf_nsd_input_template.json +++ b/src/aosm/azext_aosm/tests/latest/scenario_test_mocks/mock_input_templates/cnf_nsd_input_template.json @@ -15,7 +15,7 @@ "publisher_resource_group": "{{publisher_resource_group_name}}" } ], - "nsdg_name": "nginx", + "nsd_name": "nginx", "nsd_version": "1.0.0", "nsdv_description": "Deploys a basic NGINX CNF" } \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/scenario_test_mocks/mock_input_templates/vnf_nsd_input_template.json b/src/aosm/azext_aosm/tests/latest/scenario_test_mocks/mock_input_templates/vnf_nsd_input_template.json index b74099cb3ca..960a4b0b57b 100644 --- a/src/aosm/azext_aosm/tests/latest/scenario_test_mocks/mock_input_templates/vnf_nsd_input_template.json +++ b/src/aosm/azext_aosm/tests/latest/scenario_test_mocks/mock_input_templates/vnf_nsd_input_template.json @@ -15,7 +15,7 @@ "publisher_resource_group": "{{publisher_resource_group_name}}" } ], - "nsdg_name": "ubuntu", + "nsd_name": "ubuntu", "nsd_version": "1.0.0", "nsdv_description": "Plain ubuntu VM" } \ No newline at end of file